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