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