2006-03-26 [colin] 2.0.0cvs172
[claws.git] / src / pine.c
1 /*
2  * Sylpheed -- a GTK+ based, lightweight, and fast e-mail client
3  * Copyright (C) 2002-2006 Match Grun and the Sylpheed-Claws team
4  *
5  * This program is free software; you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation; either version 2 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program; if not, write to the Free Software
17  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18  */
19
20 /*
21  * Functions necessary to access Pine address book file.
22  */
23
24 #include <sys/stat.h>
25 #include <glib.h>
26 #include <string.h>
27
28 #include "utils.h"
29 #include "mgutils.h"
30 #include "pine.h"
31 #include "addritem.h"
32 #include "addrcache.h"
33
34 #define PINE_HOME_FILE  ".addressbook"
35 #define PINEBUFSIZE     2048
36 #define CHAR_QUOTE      '\"'
37 #define CHAR_APOS       '\''
38 #define CHAR_COMMA      ','
39 #define CHAR_AT         '@'
40
41 /*
42 * Create new object.
43 */
44 PineFile *pine_create() {
45         PineFile *pineFile;
46         pineFile = g_new0( PineFile, 1 );
47         pineFile->path = NULL;
48         pineFile->file = NULL;
49         pineFile->retVal = MGU_SUCCESS;
50         pineFile->uniqTable = g_hash_table_new( g_str_hash, g_str_equal );
51         pineFile->cbProgress = NULL;
52         return pineFile;
53 }
54
55 /*
56 * Properties...
57 */
58 void pine_set_file( PineFile* pineFile, const gchar *value ) {
59         g_return_if_fail( pineFile != NULL );
60         pineFile->path = mgu_replace_string( pineFile->path, value );
61         g_strstrip( pineFile->path );
62 }
63
64 /*
65 * Register a callback function. When called, the function will be passed
66 * the following arguments:
67 *       PineFile object,
68 *       File size (long),
69 *       Current position (long)
70 * This can be used for a progress indicator.
71 */
72 void pine_set_callback( PineFile *pineFile, void *func ) {
73         pineFile->cbProgress = func;
74 }
75
76 /*
77  * Free key in table.
78  */
79 static gint pine_free_table_vis( gpointer key, gpointer value, gpointer data ) {
80         g_free( key );
81         key = NULL;
82         value = NULL;
83         return TRUE;
84 }
85
86 /*
87 * Free up object by releasing internal memory.
88 */
89 void pine_free( PineFile *pineFile ) {
90         g_return_if_fail( pineFile != NULL );
91
92         /* Close file */
93         if( pineFile->file ) fclose( pineFile->file );
94
95         /* Free internal stuff */
96         g_free( pineFile->path );
97
98         /* Free unique address table */
99         g_hash_table_foreach_remove( pineFile->uniqTable, pine_free_table_vis, NULL );
100         g_hash_table_destroy( pineFile->uniqTable );
101
102         /* Clear pointers */
103         pineFile->file = NULL;
104         pineFile->path = NULL;
105         pineFile->retVal = MGU_SUCCESS;
106         pineFile->uniqTable = NULL;
107         pineFile->cbProgress = NULL;
108
109         /* Now release file object */
110         g_free( pineFile );
111 }
112
113 /*
114  * Display object to specified stream.
115  * Enter: pineFile File object.
116  *        stream   File..
117  */
118 void pine_print_file( PineFile *pineFile, FILE *stream ) {
119         g_return_if_fail( pineFile != NULL );
120         fprintf( stream, "Pine File:\n" );
121         fprintf( stream, "file spec: '%s'\n", pineFile->path );
122         fprintf( stream, "  ret val: %d\n",   pineFile->retVal );
123 }
124
125 /*
126  * Open file for read.
127  * Enter: pineFile File object.
128  * return: TRUE if file opened successfully.
129  */
130 static gint pine_open_file( PineFile* pineFile ) {
131         if( pineFile->path ) {
132                 pineFile->file = g_fopen( pineFile->path, "rb" );
133                 if( ! pineFile->file ) {
134                         pineFile->retVal = MGU_OPEN_FILE;
135                         return pineFile->retVal;
136                 }
137         }
138         else {
139                 /* printf( "file not specified\n" ); */
140                 pineFile->retVal = MGU_NO_FILE;
141                 return pineFile->retVal;
142         }
143
144         /* Setup a buffer area */
145         pineFile->retVal = MGU_SUCCESS;
146         return pineFile->retVal;
147 }
148
149 /*
150  * Close file.
151  * Enter: pineFile File object.
152  */
153 static void pine_close_file( PineFile *pineFile ) {
154         g_return_if_fail( pineFile != NULL );
155         if( pineFile->file ) fclose( pineFile->file );
156         pineFile->file = NULL;
157 }
158
159 /*
160  * Read line of text from file.
161  * Enter: pineFile File object.
162  * Return: Copy of buffer. Should be g_free'd when done.
163  */
164 static gchar *pine_read_line( PineFile *pineFile ) {
165         gchar buf[ PINEBUFSIZE ];
166         int c, i = 0;
167         gchar ch;
168
169         if( feof( pineFile->file ) ) 
170                 return NULL;
171
172         while( i < PINEBUFSIZE-1 ) {
173                 c = fgetc( pineFile->file );
174                 if( c == EOF ) {
175                         if( i == 0 ) 
176                                 return NULL;
177                         break;
178                 }
179                 ch = (gchar) c;
180                 if( ch == '\0' ) {
181                         if( i == 0 ) 
182                                 return NULL;
183                         break;
184                 }
185                 if( ch == '\n' ) {
186                         break;
187                 }
188                 buf[i] = ch;
189                 i++;
190         }
191         buf[i] = '\0';
192
193         /* Copy into private buffer */
194         return g_strdup( buf );
195 }
196
197 /*
198  * Parsed address data.
199  */
200 typedef struct _Pine_ParsedRec_ Pine_ParsedRec;
201 struct _Pine_ParsedRec_ {
202         gchar *nickName;
203         gchar *name;
204         gchar *address;
205         gchar *fcc;
206         gchar *comments;
207         gboolean isGroup;
208         GSList *listName;
209         GSList *listAddr;
210 };
211
212 /*
213  * Free data record.
214  * Enter: rec Data record.
215  */
216 static void pine_free_rec( Pine_ParsedRec *rec ) {
217         if( rec ) {
218                 g_free( rec->nickName );
219                 g_free( rec->name );
220                 g_free( rec->address );
221                 g_free( rec->fcc );
222                 g_free( rec->comments );
223                 mgu_clear_slist( rec->listName );
224                 mgu_clear_slist( rec->listAddr );
225                 g_slist_free( rec->listName );
226                 g_slist_free( rec->listAddr );
227                 rec->nickName = NULL;
228                 rec->name = NULL;
229                 rec->address = NULL;
230                 rec->fcc = NULL;
231                 rec->comments = NULL;
232                 rec->isGroup = FALSE;
233                 g_free( rec );
234         }
235 }
236
237 /*
238  * Clean name.
239  */
240 static void pine_clean_name( Pine_ParsedRec *rec ) {
241         gchar *p;
242
243         p = rec->name;
244         if( p == NULL ) return;
245         if( *p == '\0' ) return;
246
247         g_strstrip( rec->name );
248         if( *p == CHAR_APOS || *p == CHAR_QUOTE ) {
249                 return;
250         }
251
252         /* If embedded comma present, surround match with quotes */
253         while( *p ) {
254                 if( *p == CHAR_COMMA ) {
255                         p = g_strdup_printf( "\"%s\"", rec->name );
256                         g_free( rec->name );
257                         rec->name = p;
258                         return;
259                 }
260                 p++;
261         }
262 }
263
264 /*
265  * Parse pine address record.
266  * Enter:  buf Address record buffer.
267  * Return: Data record.
268  */
269 static Pine_ParsedRec *pine_parse_record( gchar *buf ) {
270         Pine_ParsedRec *rec;
271         gchar *p, *f;
272         gint pos, len, i;
273         gchar *tmp[5];
274
275         for( i = 0; i < 5; i++ )
276                 tmp[i] = NULL;
277
278         /* Extract tab separated values */
279         rec = NULL;
280         pos = 0;
281         p = f = buf;
282         while( *p ) {
283                 if( *p == '\t' ) {
284                         len = p - f;
285                         if( len > 0 ) {
286                                 tmp[ pos ] = g_strndup( f, len );
287                                 f = p;
288                                 f++;
289                         }
290                         pos++;
291                 }
292                 p++;
293         }
294
295         /* Extract last value */
296         len = p - f;
297         if( len > 0 ) {
298                 tmp[ pos++ ] = g_strndup( f, len );
299         }
300
301         /* Populate record */
302         if( pos > 0 ) {
303                 rec = g_new0( Pine_ParsedRec, 1 );
304                 rec->isGroup = FALSE;
305                 for( i = 0; i < pos; i++ ) {
306                         f = tmp[i];
307                         if( f ) {
308                                 g_strstrip( f );
309                         }
310                         if( i == 0 ) rec->nickName = f;
311                         else if( i == 1 ) rec->name = f;
312                         else if( i == 2 ) rec->address = f;
313                         else if( i == 3 ) rec->fcc = f;
314                         else if( i == 4 ) rec->comments = f;
315                         tmp[i] = NULL;
316                 }
317
318                 if( rec->address != NULL ) {
319                         /* Strip leading/trailing parens */
320                         p = rec->address;
321                         if( *p == '(' ) {
322                                 len = strlen( p ) - 1;
323                                 *p = ' ';
324                                 *(p + len) = ' ';
325                                 rec->isGroup = TRUE;
326                         }
327                 }
328         }
329
330         return rec;
331 }
332
333 /*
334  * Parse name from email address string.
335  * Enter: buf Start address of buffer to process (not modified).
336  *        atp Pointer to email at (@) character.
337  *        ap  Pointer to start of email address returned.
338  *        ep  Pointer to end of email address returned.
339  * Return: Parsed name or NULL if not present. This should be g_free'd
340  * when done.
341  */
342 static gchar *pine_parse_name(
343                 const gchar *buf, const gchar *atp, const gchar **ap,
344                 const gchar **ep )
345 {
346         gchar *name;
347         const gchar *pos;
348         const gchar *tmp;
349         const gchar *bp;
350         gint ilen;
351
352         name = NULL;
353         *ap = NULL;
354         *ep = NULL;
355
356         /* Find first non-separator char */
357         bp = buf;
358         while( TRUE ) {
359                 if( strchr( ",; \n\r", *bp ) == NULL ) break;
360                 bp++;
361         }
362
363         /* Search back for start of name */
364         tmp = atp;
365         pos = atp;
366         while( pos >= bp ) {
367                 tmp = pos;
368                 if( *pos == '<' ) {
369                         /* Found start of address/end of name part */
370                         ilen = -1 + ( size_t ) ( pos - bp );
371                         name = g_strndup( bp, ilen + 1 );
372                         *(name + ilen + 1) = '\0';
373
374                         /* Remove leading trailing quotes and spaces */
375                         mgu_str_ltc2space( name, '\"', '\"' );
376                         mgu_str_ltc2space( name, '\'', '\'' );
377                         mgu_str_ltc2space( name, '\"', '\"' );
378                         mgu_str_unescape( name );
379                         g_strstrip( name );
380                         break;
381                 }
382                 pos--;
383         }
384         *ap = tmp;
385
386         /* Search forward for end of address */
387         pos = atp + 1;
388         while( TRUE ) {
389                 if( *pos == '>' ) {
390                         pos++;
391                         break;
392                 }
393                 if( strchr( ",; \'\n\r", *pos ) ) break;
394                 pos++;
395         }
396         *ep = pos;
397
398         return name;
399 }
400
401 /*
402  * Parse address list.
403  * Enter: pineFile Pine control data.
404  *        cache    Address cache.
405  *        rec      Data record.
406  */
407 static void pine_parse_address( PineFile *pineFile, AddressCache *cache, Pine_ParsedRec *rec ) {
408         const gchar *buf;
409         gchar addr[ PINEBUFSIZE ];
410         const gchar *bp;
411         const gchar *ep;
412         gchar *atCh;
413         gchar *name;
414         gint len;
415
416         g_return_if_fail( rec->address != NULL );
417
418         buf = rec->address;
419         while((atCh = strchr( buf, CHAR_AT )) != NULL) {
420                 name = pine_parse_name( buf, atCh, &bp, &ep );
421                 len = ( size_t ) ( ep - bp );
422                 strncpy( addr, bp, len );
423                 addr[ len ] = '\0';
424                 extract_address( addr );
425
426                 if( name == NULL ) name = g_strdup( "" );
427                 rec->listName = g_slist_append( rec->listName, name );
428                 rec->listAddr = g_slist_append( rec->listAddr, g_strdup( addr ) );
429
430                 buf = ep;
431                 if( atCh == ep ) {
432                         buf++;
433                 }
434         }
435 }
436
437 /*
438  * Insert person and address into address cache.
439  * Enter: pineFile Pine control data.
440  *        cache    Address cache.
441  *        address  E-Mail address.
442  *        name     Name.
443  *        remarks  Remarks.
444  * Return: E-Mail object, either inserted or found in hash table.
445  */
446 static ItemEMail *pine_insert_table(
447                 PineFile *pineFile, AddressCache *cache, gchar *address,
448                 gchar *name, gchar *remarks )
449 {
450         ItemPerson *person;
451         ItemEMail *email;
452         gchar *key;
453
454         g_return_val_if_fail( address != NULL, NULL );
455
456         /* create an entry with empty name if needed */
457         if ( name == NULL )
458                 name = "";
459
460         /* Test whether address already in hash table */
461         key = g_strdup( address );
462         g_strdown( key );
463         email = g_hash_table_lookup( pineFile->uniqTable, key );
464
465         if( email == NULL ) {
466                 /* No - create person */
467                 person = addritem_create_item_person();
468                 addritem_person_set_common_name( person, name );
469                 addrcache_id_person( cache, person );
470                 addrcache_add_person( cache, person );
471
472                 /* Add email for person */
473                 email = addritem_create_item_email();
474                 addritem_email_set_address( email, address );
475                 addritem_email_set_remarks( email, remarks );
476                 addrcache_id_email( cache, email );
477                 addrcache_person_add_email( cache, person, email );
478
479                 /* Insert entry */
480                 g_hash_table_insert( pineFile->uniqTable, key, email );
481         }
482         else {
483                 /* Yes - update person with longest name */
484                 person = ( ItemPerson * ) ADDRITEM_PARENT(email);
485                 if( strlen( name ) > strlen( ADDRITEM_NAME(person) ) ) {
486                         addritem_person_set_common_name( person, name );
487                 }
488
489                 /* Free up */
490                 g_free( key );
491         }
492
493         return email;
494 }
495
496 /*
497  * Parse address line adn build address items.
498  * Enter: pineFile Pine control data.
499  *        cache    Address cache to load.
500  *        line     Address record.
501  */
502 static void pine_build_items( PineFile *pineFile, AddressCache *cache, gchar *line ) {
503         Pine_ParsedRec *rec;
504         GSList *nodeAddr, *nodeName;
505         ItemGroup *group;
506         ItemEMail *email;
507
508         rec = pine_parse_record( line );
509         if( rec ) {
510                 pine_clean_name( rec );
511                 pine_parse_address( pineFile, cache, rec );
512                 /* pine_print_rec( rec, stdout ); */
513                 /* printf( "=========\n" ); */
514
515                 if( rec->isGroup ) {
516                         /* Create group */
517                         group = addritem_create_item_group();
518                         addritem_group_set_name( group, rec->nickName );
519                         addrcache_id_group( cache, group );
520                         addrcache_add_group( cache, group );
521
522                         /* Add email to group */
523                         nodeName = rec->listName;
524                         nodeAddr = rec->listAddr;
525                         while( nodeAddr ) {
526                                 email = pine_insert_table(
527                                                 pineFile, cache, nodeAddr->data,
528                                                 nodeName->data, "" );
529
530                                 /* Add email to group */
531                                 addritem_group_add_email( group, email );
532
533                                 nodeAddr = g_slist_next( nodeAddr );
534                                 nodeName = g_slist_next( nodeName );
535                         }
536                 }
537                 else {
538                         email = pine_insert_table(
539                                         pineFile, cache, rec->address,
540                                         rec->name, rec->comments );
541                 }
542
543                 pine_free_rec( rec );
544         }
545 }
546
547 /*
548  * Read file data into address cache.
549  * Enter: pineFile Pine control data.
550  *        cache    Address cache to load.
551  */
552 static void pine_read_file( PineFile *pineFile, AddressCache *cache ) {
553         GSList *listValue = NULL;
554         gboolean flagEOF = FALSE, flagProc = FALSE, flagDone = FALSE;
555         gchar *line =  NULL, *lineValue = NULL;
556         long posEnd = 0L;
557         long posCur = 0L;
558
559         /* Find EOF for progress indicator */
560         fseek( pineFile->file, 0L, SEEK_END );
561         posEnd = ftell( pineFile->file );
562         fseek( pineFile->file, 0L, SEEK_SET );
563
564         flagProc = FALSE;
565         while( ! flagDone ) {
566                 if( flagEOF ) {
567                         flagDone = TRUE;
568                         flagProc = TRUE;
569                 }
570                 else {
571                         line =  pine_read_line( pineFile );
572                 }
573
574                 posCur = ftell( pineFile->file );
575                 if( pineFile->cbProgress ) {
576                         /* Call progress indicator */
577                         ( pineFile->cbProgress ) ( pineFile, & posEnd, & posCur );
578                 }
579
580                 /* Add line to list */
581                 if( line == NULL ) {
582                         flagEOF = TRUE;
583                 }
584                 else {
585                         /* Check for continuation line (1 space only) */
586                         if( *line == ' ' ) {
587                                 g_strchug( line );
588                                 listValue = g_slist_append(
589                                                 listValue, g_strdup( line ) );
590                                 flagProc = FALSE;
591                         }
592                         else {
593                                 flagProc = TRUE;
594                         }
595                 }
596
597                 if( flagProc ) {
598                         if( listValue != NULL ) {
599                                 /* Process list */
600                                 lineValue = mgu_list_coalesce( listValue );
601                                 if( lineValue ) {
602                                         pine_build_items(
603                                                 pineFile, cache, lineValue );
604                                 }
605                                 g_free( lineValue );
606                                 lineValue = NULL;
607                                 mgu_free_list( listValue );
608                                 listValue = NULL;
609                         }
610                         if( line != NULL ) {
611                                 /* Append to list */
612                                 listValue = g_slist_append(
613                                                 listValue, g_strdup( line ) );
614                         }
615                 }
616
617                 g_free( line );
618                 line = NULL;
619         }
620
621         /* Release data */
622         mgu_free_list( listValue );
623         listValue = NULL;
624 }
625
626 /*
627  * ============================================================================================
628  * Read file into list. Main entry point
629  * Enter:  pineFile Pine control data.
630  *         cache    Address cache to load.
631  * Return: Status code.
632  * ============================================================================================
633  */
634 gint pine_import_data( PineFile *pineFile, AddressCache *cache ) {
635         g_return_val_if_fail( pineFile != NULL, MGU_BAD_ARGS );
636         g_return_val_if_fail( cache != NULL, MGU_BAD_ARGS );
637
638         pineFile->retVal = MGU_SUCCESS;
639         addrcache_clear( cache );
640         cache->dataRead = FALSE;
641         pine_open_file( pineFile );
642         if( pineFile->retVal == MGU_SUCCESS ) {
643                 /* Read data into the cache */
644                 pine_read_file( pineFile, cache );
645                 pine_close_file( pineFile );
646
647                 /* Mark cache */
648                 cache->modified = FALSE;
649                 cache->dataRead = TRUE;
650         }
651         return pineFile->retVal;
652 }
653
654 #define WORK_BUFLEN 1024
655
656 /*
657  * Attempt to find a Pine addressbook file.
658  * Return: Filename, or home directory if not found, or empty string if
659  * no home. Filename should be g_free() when done.
660  */
661 gchar *pine_find_file( void ) {
662         const gchar *homedir;
663         gchar str[ WORK_BUFLEN ];
664         gint len;
665         FILE *fp;
666
667         homedir = g_get_home_dir();
668         if( ! homedir ) return g_strdup( "" );
669
670         strcpy( str, homedir );
671         len = strlen( str );
672         if( len > 0 ) {
673                 if( str[ len-1 ] != G_DIR_SEPARATOR ) {
674                         str[ len ] = G_DIR_SEPARATOR;
675                         str[ ++len ] = '\0';
676                 }
677         }
678         strcat( str, PINE_HOME_FILE );
679
680         /* Attempt to open */
681         if( ( fp = g_fopen( str, "rb" ) ) != NULL ) {
682                 fclose( fp );
683         }
684         else {
685                 /* Truncate filename */
686                 str[ len ] = '\0';
687         }
688         return g_strdup( str );
689 }
690
691 /*
692 * End of Source.
693 */
694