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