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