]> Pileus Git - ~andy/fetchmail/blob - uid.c
Update <sq> Albanian translation to fetchmail-6.4.3-rc2
[~andy/fetchmail] / uid.c
1 /**
2  * \file uid.c -- UIDL handling for POP3 servers without LAST
3  *
4  * For license terms, see the file COPYING in this directory.
5  */
6
7 #include "config.h"
8
9 #include <sys/stat.h>
10 #include <errno.h>
11 #include <stdio.h>
12 #include <limits.h>
13 #if defined(STDC_HEADERS)
14 #include <stdlib.h>
15 #include <string.h>
16 #endif
17 #if defined(HAVE_UNISTD_H)
18 #include <unistd.h>
19 #endif
20
21 #include "fetchmail.h"
22 #include "i18n.h"
23 #include "sdump.h"
24
25 /*
26  * Machinery for handling UID lists live here.  This is currently used
27  * by POP3, but may also be useful for making the IMAP4 querying logic
28  * UID-oriented.
29  *
30  * These functions are also used by the rest of the code to maintain
31  * string lists.
32  *
33  * Here's the theory:
34  *
35  * At start of a query, we have a (possibly empty) list of UIDs to be
36  * considered seen in `oldsaved'.  These are messages that were left in
37  * the mailbox and *not deleted* on previous queries (we don't need to
38  * remember the UIDs of deleted messages because ... well, they're gone!)
39  * This list is initially set up by initialize_saved_list() from the
40  * .fetchids file.
41  *
42  * Early in the query, during the execution of the protocol-specific
43  * getrange code, the driver expects that the host's `newsaved' member
44  * will be filled with a list of UIDs and message numbers representing
45  * the mailbox state.  If this list is empty, the server did
46  * not respond to the request for a UID listing.
47  *
48  * Each time a message is fetched, we can check its UID against the
49  * `oldsaved' list to see if it is old.
50  *
51  * Each time a message-id is seen, we mark it with MARK_SEEN.
52  *
53  * Each time a message is deleted, we mark its id UID_DELETED in the
54  * `newsaved' member.  When we want to assert that an expunge has been
55  * done on the server, we call expunge_uid() to register that all
56  * deleted messages are gone by marking them UID_EXPUNGED.
57  *
58  * At the end of the query, the `newsaved' member becomes the
59  * `oldsaved' list.  The old `oldsaved' list is freed.
60  *
61  * At the end of the fetchmail run, seen and non-EXPUNGED members of all
62  * current `oldsaved' lists are flushed out to the .fetchids file to
63  * be picked up by the next run.  If there are no un-expunged
64  * messages, the file is deleted.
65  *
66  * One disadvantage of UIDL is that all the UIDs have to be downloaded
67  * before a search for new messages can be done. Typically, new messages
68  * are appended to mailboxes. Hence, downloading all UIDs just to download
69  * a few new mails is a waste of bandwidth. If new messages are always at
70  * the end of the mailbox, fast UIDL will decrease the time required to
71  * download new mails.
72  *
73  * During fast UIDL, the UIDs of all messages are not downloaded! The first
74  * unseen message is searched for by using a binary search on UIDs. UIDs
75  * after the first unseen message are downloaded as and when needed.
76  *
77  * The advantages of fast UIDL are (this is noticeable only when the
78  * mailbox has too many mails):
79  *
80  * - There is no need to download the UIDs of all mails right at the start.
81  * - There is no need to save all the UIDs in memory separately in
82  * `newsaved' list.
83  * - There is no need to download the UIDs of seen mail (except for the
84  * first binary search).
85  * - The first new mail is downloaded considerably faster.
86  *
87  * The disadvantages are:
88  *
89  * - Since all UIDs are not downloaded, it is not possible to swap old and
90  * new list. The current state of the mailbox is essentially a merged state
91  * of old and new mails.
92  * - If an intermediate mail has been temporarily refused (say, due to 4xx
93  * code from the smtp server), this mail may not get downloaded.
94  * - If 'flush' is used, such intermediate mails will also get deleted.
95  *
96  * The first two disadvantages can be overcome by doing a linear search
97  * once in a while (say, every 10th poll). Also, with flush, fast UIDL
98  * should be disabled.
99  *
100  * Note: some comparisons (those used for DNS address lists) are caseblind!
101  */
102
103 int dofastuidl = 0;
104
105 #ifdef POP3_ENABLE
106 /** UIDs associated with un-queried hosts */
107 static struct idlist *scratchlist;
108
109 /** Read saved IDs from \a idfile and attach to each host in \a hostlist. */
110 static int dump_saved_uid(struct uid_db_record *rec, void *unused)
111 {
112     char *t;
113
114     (void)unused;
115
116     t = sdump(rec->id, rec->id_len);
117     report_build(stdout, " %s\n", t);
118     free(t);
119
120     return 0;
121 }
122
123 /** Read saved IDs from \a idfile and attach to each host in \a hostlist.
124  * Returns 0 for success, or a non-zero error code. */
125 int initialize_saved_lists(struct query *hostlist, const char *idfile)
126 {
127     struct stat statbuf;
128     FILE        *tmpfp;
129     struct query *ctl;
130     int  err;
131
132     /* make sure lists are initially empty */
133     for (ctl = hostlist; ctl; ctl = ctl->next) {
134         ctl->skipped = (struct idlist *)NULL;
135
136         init_uid_db(&ctl->oldsaved);
137         init_uid_db(&ctl->newsaved);
138     }
139
140     errno = 0;
141
142     /*
143      * Croak if the uidl directory does not exist.
144      * This probably means an NFS mount failed and we can't
145      * see a uidl file that ought to be there.
146      * Question: is this a portable check? It's not clear
147      * that all implementations of lstat() will return ENOTDIR
148      * rather than plain ENOENT in this case...
149      */
150     if (lstat(idfile, &statbuf) < 0) {
151         if (errno == ENOTDIR)
152         {
153             report(stderr, "lstat: %s: %s\n", idfile, strerror(errno));
154             return PS_IOERR;
155         }
156     }
157
158     /* let's get stored message UIDs from previous queries */
159     if ((tmpfp = fopen(idfile, "r")) != (FILE *)NULL)
160     {
161         char buf[POPBUFSIZE+1];
162         char *host = NULL;      /* pacify -Wall */
163         char *user;
164         char *id;
165         char *atsign;   /* temp pointer used in parsing user and host */
166         char *delimp1;
167         char saveddelim1;
168         char *delimp2;
169         char saveddelim2 = '\0';        /* pacify -Wall */
170
171         while (fgets(buf, POPBUFSIZE, tmpfp) != (char *)NULL)
172         {
173             /*
174              * At this point, we assume the bug has two fields -- a user@host
175              * part, and an ID part. Either field may contain spurious @ signs.
176              * The previous version of this code presumed one could split at
177              * the rightmost '@'.  This is not correct, as InterMail puts an
178              * '@' in the UIDL.
179              */
180
181             /* first, skip leading spaces */
182             user = buf + strspn(buf, " \t");
183
184             /*
185              * First, we split the buf into a userhost part and an id
186              * part ... but id doesn't necessarily start with a '<',
187              * espescially if the POP server returns an X-UIDL header
188              * instead of a Message-ID, as GMX's (www.gmx.net) POP3
189              * StreamProxy V1.0 does.
190              *
191              * this is one other trick. The userhost part
192              * may contain ' ' in the user part, at least in
193              * the lotus notes case.
194              * So we start looking for the '@' after which the
195              * host will follow with the ' ' separator with the id.
196              *
197              * XXX FIXME: There is a case this code cannot handle:
198              * the user name cannot have blanks after a '@'.
199              */
200             if ((delimp1 = strchr(user, '@')) != NULL &&
201                 (id = strchr(delimp1,' ')) != NULL)
202             {
203                 for (delimp1 = id; delimp1 >= user; delimp1--)
204                     if ((*delimp1 != ' ') && (*delimp1 != '\t'))
205                         break;
206
207                 /*
208                  * It should be safe to assume that id starts after
209                  * the " " - after all, we're writing the " "
210                  * ourselves in write_saved_lists() :-)
211                  */
212                 id = id + strspn(id, " ");
213
214                 delimp1++; /* but what if there is only white space ?!? */
215                 /* we have at least one @, else we are not in this branch */
216                 saveddelim1 = *delimp1;         /* save char after token */
217                 *delimp1 = '\0';                /* delimit token with \0 */
218
219                 /* now remove trailing white space chars from id */
220                 if ((delimp2 = strpbrk(id, " \t\n")) != NULL ) {
221                     saveddelim2 = *delimp2;
222                     *delimp2 = '\0';
223                 }
224
225                 atsign = strrchr(user, '@');
226                 /* we have at least one @, else we are not in this branch */
227                 *atsign = '\0';
228                 host = atsign + 1;
229
230                 /* find uidl db and save it */
231                 for (ctl = hostlist; ctl; ctl = ctl->next) {
232                     if (strcasecmp(host, ctl->server.queryname) == 0
233                             && strcasecmp(user, ctl->remotename) == 0) {
234                         uid_db_insert(&ctl->oldsaved, id, UID_SEEN);
235                         break;
236                     }
237                 }
238                 /*
239                  * If it's not in a host we're querying,
240                  * save it anyway.  Otherwise we'd lose UIDL
241                  * information any time we queried an explicit
242                  * subset of hosts.
243                  */
244                 if (ctl == (struct query *)NULL) {
245                     /* restore string */
246                     *delimp1 = saveddelim1;
247                     *atsign = '@';
248                     if (delimp2 != NULL) {
249                         *delimp2 = saveddelim2;
250                     }
251                     save_str(&scratchlist, buf, UID_SEEN);
252                 }
253             }
254         }
255         err  = ferror(tmpfp);
256         err |= fclose(tmpfp);   /* not checking should be safe, mode was "r" */
257                                 /* bit-wise or, we only care about non-zero */
258     } else {
259         err = (errno != ENOENT);
260     }
261     if (err) {
262         report(stderr, GT_("Open or read error while reading idfile %s: %s\n"),
263                 idfile, strerror(errno));
264         return PS_IOERR;
265     }
266
267     if (outlevel >= O_DEBUG)
268     {
269         struct idlist   *idp;
270
271         for (ctl = hostlist; ctl; ctl = ctl->next)
272             {
273                 report_build(stdout, GT_("Old UID list from %s:\n"),
274                              ctl->server.pollname);
275
276                 if (!uid_db_n_records(&ctl->oldsaved))
277                     report_build(stdout, "%s\n", GT_(" <empty>"));
278                 else
279                     traverse_uid_db(&ctl->oldsaved, dump_saved_uid, NULL);
280
281                 report_complete(stdout, "\n");
282             }
283
284         report_build(stdout, GT_("Scratch list of UIDs:\n"));
285         if (!scratchlist)
286                 report_build(stdout, "%s\n", GT_(" <empty>"));
287         else for (idp = scratchlist; idp; idp = idp->next) {
288                 char *t = sdump(idp->id, strlen(idp->id)-1);
289                 report_build(stdout, " %s\n", t);
290                 free(t);
291         }
292         report_complete(stdout, "\n");
293     }
294     return PS_SUCCESS;
295 }
296
297 /** Assert that all UIDs marked deleted in query \a ctl have actually been
298 expunged. */
299 static int mark_as_expunged_if(struct uid_db_record *rec, void *unused)
300 {
301     (void)unused;
302
303     if (rec->status == UID_DELETED) rec->status = UID_EXPUNGED;
304     return 0;
305 }
306
307 void expunge_uids(struct query *ctl)
308 {
309     traverse_uid_db(dofastuidl ? &ctl->oldsaved : &ctl->newsaved,
310                      mark_as_expunged_if, NULL);
311 }
312
313 static const char *str_uidmark(int mark)
314 {
315         static char buf[20];
316
317         switch(mark) {
318                 case UID_UNSEEN:
319                         return "UNSEEN";
320                 case UID_SEEN:
321                         return "SEEN";
322                 case UID_EXPUNGED:
323                         return "EXPUNGED";
324                 case UID_DELETED:
325                         return "DELETED";
326                 default:
327                         if (snprintf(buf, sizeof(buf), "MARK=%d", mark) < 0)
328                                 return "ERROR";
329                         else
330                                 return buf;
331         }
332 }
333
334 static int dump_uid_db_record(struct uid_db_record *rec, void *arg)
335 {
336         unsigned *n_recs;
337         char *t;
338
339         n_recs = (unsigned int *)arg;
340         --*n_recs;
341
342         t = sdump(rec->id, rec->id_len);
343         report_build(stdout, " %s = %s\n", t, str_uidmark(rec->status));
344         free(t);
345
346         return 0;
347 }
348
349 static void dump_uid_db(struct uid_db *db)
350 {
351         unsigned n_recs;
352
353         n_recs = uid_db_n_records(db);
354         if (!n_recs) {
355                 report_build(stdout, GT_(" <empty>"));
356                 return;
357         }
358
359         traverse_uid_db(db, dump_uid_db_record, &n_recs);
360 }
361
362 /** Finish a successful query */
363 void uid_swap_lists(struct query *ctl)
364 {
365     /* debugging code */
366     if (outlevel >= O_DEBUG)
367     {
368         if (dofastuidl) {
369             report_build(stdout, GT_("Merged UID list from %s:\n"), ctl->server.pollname);
370             dump_uid_db(&ctl->oldsaved);
371         } else {
372             report_build(stdout, GT_("New UID list from %s:\n"), ctl->server.pollname);
373             dump_uid_db(&ctl->newsaved);
374         }
375         report_complete(stdout, "\n");
376     }
377
378     /*
379      * Don't swap UID lists unless we've actually seen UIDLs.
380      * This is necessary in order to keep UIDL information
381      * from being heedlessly deleted later on.
382      *
383      * Older versions of fetchmail did
384      *
385      *     free_str_list(&scratchlist);
386      *
387      * after swap.  This was wrong; we need to preserve the UIDL information
388      * from unqueried hosts.  Unfortunately, not doing this means that
389      * under some circumstances UIDLs can end up being stored forever --
390      * specifically, if a user description is removed from .fetchmailrc
391      * with UIDLs from that account in .fetchids, there is no way for
392      * them to ever get garbage-collected.
393      */
394     if (uid_db_n_records(&ctl->newsaved))
395     {
396         swap_uid_db_data(&ctl->newsaved, &ctl->oldsaved);
397         clear_uid_db(&ctl->newsaved);
398     }
399     /* in fast uidl, there is no need to swap lists: the old state of
400      * mailbox cannot be discarded! */
401     else if (outlevel >= O_DEBUG && !dofastuidl)
402         report(stdout, GT_("not swapping UID lists, no UIDs seen this query\n"));
403 }
404
405 /** Finish a query which had errors */
406 void uid_discard_new_list(struct query *ctl)
407 {
408     /* debugging code */
409     if (outlevel >= O_DEBUG)
410     {
411         /* this is now a merged list! the mails which were seen in this
412          * poll are marked here. */
413         report_build(stdout, GT_("Merged UID list from %s:\n"), ctl->server.pollname);
414         dump_uid_db(&ctl->oldsaved);
415         report_complete(stdout, "\n");
416     }
417
418     if (uid_db_n_records(&ctl->newsaved))
419     {
420         /* new state of mailbox is not reliable */
421         if (outlevel >= O_DEBUG)
422             report(stdout, GT_("discarding new UID list\n"));
423         clear_uid_db(&ctl->newsaved);
424     }
425 }
426
427 /** Reset the number associated with each id */
428 void uid_reset_num(struct query *ctl)
429 {
430     reset_uid_db_nums(&ctl->oldsaved);
431 }
432
433 /** Write list of seen messages, at end of run. */
434 static int count_seen_deleted(struct uid_db_record *rec, void *arg)
435 {
436     if (rec->status == UID_SEEN || rec->status == UID_DELETED)
437         ++*(long *)arg;
438     return 0;
439 }
440
441 struct write_saved_info {
442     struct query *ctl;
443     FILE *fp;
444 };
445
446 static int write_uid_db_record(struct uid_db_record *rec, void *arg)
447 {
448     struct write_saved_info *info;
449     int rc;
450
451     if (!(rec->status == UID_SEEN || rec->status == UID_DELETED))
452         return 0;
453
454     info = (struct write_saved_info *)arg;
455     rc = fprintf(info->fp, "%s@%s %s\n",
456                  info->ctl->remotename, info->ctl->server.queryname,
457                  rec->id);
458     return rc < 0 ? -1 : 0;
459 }
460
461 /** Write new list of UIDs (state) to \a idfile. */
462 void write_saved_lists(struct query *hostlist, const char *idfile)
463 {
464     long        idcount;
465     FILE        *tmpfp;
466     struct query *ctl;
467     struct idlist *idp;
468
469     /* if all lists are empty, nuke the file */
470     idcount = 0;
471     for (ctl = hostlist; ctl; ctl = ctl->next)
472         traverse_uid_db(&ctl->oldsaved, count_seen_deleted, &idcount);
473
474     /* either nuke the file or write updated last-seen IDs */
475     if (!idcount && !scratchlist)
476     {
477         if (outlevel >= O_DEBUG) {
478             if (access(idfile, F_OK) == 0)
479                     report(stdout, GT_("Deleting fetchids file.\n"));
480         }
481         if (unlink(idfile) && errno != ENOENT)
482             report(stderr, GT_("Error deleting %s: %s\n"), idfile, strerror(errno));
483     } else {
484         char *newnam = (char *)xmalloc(strlen(idfile) + 2);
485         mode_t old_umask;
486         strcpy(newnam, idfile);
487         strcat(newnam, "_");
488         if (outlevel >= O_DEBUG)
489             report(stdout, GT_("Writing fetchids file.\n"));
490         (void)unlink(newnam); /* remove file/link first */
491         old_umask = umask(S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH | S_IXOTH);
492         if ((tmpfp = fopen(newnam, "w")) != (FILE *)NULL) {
493             struct write_saved_info info;
494             int errflg = 0;
495
496             info.fp = tmpfp;
497
498             for (ctl = hostlist; ctl; ctl = ctl->next) {
499                 info.ctl = ctl;
500
501                 if (traverse_uid_db(&ctl->oldsaved, write_uid_db_record, &info) < 0) {
502                     int e = errno;
503                     report(stderr, GT_("Write error on fetchids file %s: %s\n"), newnam, strerror(e));
504                     errflg = 1;
505                     goto bailout;
506                 }
507             }
508
509             for (idp = scratchlist; idp; idp = idp->next)
510                 if (EOF == fputs(idp->id, tmpfp)) {
511                             int e = errno;
512                             report(stderr, GT_("Write error on fetchids file %s: %s\n"), newnam, strerror(e));
513                             errflg = 1;
514                             goto bailout;
515                 }
516
517 bailout:
518             (void)fflush(tmpfp); /* return code ignored, we check ferror instead */
519             errflg |= ferror(tmpfp);
520             errflg |= fclose(tmpfp);
521             /* if we could write successfully, move into place;
522              * otherwise, drop */
523             if (errflg) {
524                 report(stderr, GT_("Error writing to fetchids file %s, old file left in place.\n"), newnam);
525                 unlink(newnam);
526             } else {
527                 if (rename(newnam, idfile)) {
528                     report(stderr, GT_("Cannot rename fetchids file %s to %s: %s\n"), newnam, idfile, strerror(errno));
529                 }
530             }
531         } else {
532             report(stderr, GT_("Cannot open fetchids file %s for writing: %s\n"), newnam, strerror(errno));
533         }
534         free(newnam);
535         (void)umask(old_umask);
536     }
537 }
538 #endif /* POP3_ENABLE */
539
540 /* uid.c ends here */