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