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