Merged revisions 11610-11649 via svnmerge from
[wvapps.git] / retchmail / wvpopclient.cc
blob127da2dd2331c14efbcf1247bf98eb3555e08583
1 /*
2 * Worldvisions Weaver Software:
3 * Copyright (C) 1997-2003 Net Integration Technologies, Inc.
5 * Avery's insanely fast alternative to Fetchmail -- Pop3 Client
7 * This code is LGPL - see the file COPYING.LIB for a full copy of the
8 * license.
9 */
11 #include <unistd.h>
12 #include "wvstring.h"
13 #include "wvpopclient.h"
14 #include "wvistreamlist.h"
15 #include "wvdigest.h"
16 #include "wvhex.h"
17 #include "strutils.h"
19 #define MAX_PROCESSES 5
20 #define MAX_REQUESTS 10
23 WvPopClient::WvPopClient(WvStream *conn, WvStringParm acct,
24 WvStringParm _password,
25 WvStringParm _deliverto, WvStringParm _mda,
26 bool _flushing, bool _apop_enable,
27 bool _apop_enable_fallback, bool _explode,
28 bool _safemode, bool _ignorerp)
29 : WvStreamClone(conn), sendprocs(10),
30 username(acctparse(acct)), // I hate constructors!
31 password(_password), deliverto(_deliverto), mda(_mda),
32 log(WvString("PopRetriever %s", acct), WvLog::Debug3),
33 not_found(false)
35 uses_continue_select = true;
36 personal_stack_size = 65536;
37 never_select = false;
38 flushing = _flushing;
39 apop_enable = _apop_enable;
40 apop_enable_fallback = _apop_enable_fallback;
41 next_req = next_ack = sendmails = 0;
42 explode = _explode;
43 safemode = _safemode;
44 ignorerp = _ignorerp;
45 max_requests = MAX_REQUESTS;
47 if (safemode)
48 max_requests = 1;
49 safe_deletes.zap();
51 log(WvLog::Info, "Retrieve mail from %s into %s.\n", acct, deliverto);
55 WvPopClient::~WvPopClient()
57 if (isok()) // someone is killing us before our time
58 seterr("Closed connection at user request.");
60 terminate_continue_select();
62 // disable callbacks for all remaining sendmail procs
63 WvSendmailProcDict::Iter i(sendprocs);
64 for (i.rewind(); i.next(); )
65 i->cb = WvSendmailCallback();
67 if (geterr())
68 log(WvLog::Error, "Aborted. Error was: %s\n", errstr());
69 else
70 log(WvLog::Info, "Done.\n");
72 safe_deletes.zap();
76 WvString WvPopClient::acctparse(WvStringParm acct)
78 WvString u(acct);
79 char *cptr = strrchr(u.edit(), '@');
81 if (cptr)
82 *cptr = 0;
83 return u;
87 void WvPopClient::cmd(WvStringParm s)
89 // If we're already dead, we should try doing anything...
90 if (!isok())
91 return;
93 print("%s\r\n", s);
95 if (!strncasecmp(s, "pass ", 5))
96 trace.append(new WvString("pass"), true);
97 else if (!strncasecmp(s, "apop ", 5))
98 trace.append(new WvString("apop"), true);
99 else
101 if (!strncasecmp(s, "apop ", 5))
102 trace.append(new WvString("apop"), true);
103 else
104 trace.append(new WvString(s), true);
107 if (!strncasecmp(s, "retr ", 5)) // less important
108 log(WvLog::Debug3, "Send: %s\n", s);
109 else if (!strncasecmp(s, "pass ", 5)) // hide passwords
110 log(WvLog::Debug2, "Send: pass <NOT SHOWN>\n");
111 else
112 log(WvLog::Debug2, "Send: %s\n", s);
116 bool WvPopClient::response()
118 flush(0);
120 if (!isok())
122 log(WvLog::Critical,"Bailing out since the connection died: %s\n",
123 errstr());
124 return false;
127 assert(!trace.isempty());
128 WvStringList::Iter i(trace);
129 i.rewind(); i.next();
131 char *line = blocking_getline(60*1000);
132 if (!line)
134 if (isok())
135 seterr(ETIMEDOUT);
136 return false;
139 log(WvLog::Debug2, "Recv(%s): %s\n", *i, line);
140 i.unlink();
142 if (!strncmp(line, "+OK", 3))
144 res1 = res2 = 0;
145 sscanf(line, "+OK %ld %ld", &res1, &res2);
146 return true;
149 return false;
153 static int messcompare(const WvPopClient::MsgInfo *a,
154 const WvPopClient::MsgInfo *b)
156 long base = 10240;
157 long alen = a->len / base, blen = b->len / base;
158 return (alen*base + a->num) - (blen*base + b->num);
162 void WvPopClient::pre_select(SelectInfo &si)
164 bool oldrd = si.wants.readable;
166 if (never_select)
167 si.wants.readable = false;
168 WvStreamClone::pre_select(si);
169 si.wants.readable = oldrd;
173 bool WvPopClient::post_select(SelectInfo &si)
175 bool val, oldrd = si.wants.readable;
177 if (never_select)
178 si.wants.readable = false;
179 val = WvStreamClone::post_select(si);
180 si.wants.readable = oldrd;
181 return val;
185 void WvPopClient::execute()
187 const char format[] = "%20.20s> %-40.40s\n";
188 char *line, *greeting, *start, *end, *cptr;
189 int count, nmsgs;
190 WvString from, subj;
191 bool printed, in_head, msgdone;
193 WvStreamClone::execute();
195 // read hello header
196 // Adapted for APOP by markj@luminas.co.uk
197 // trace.append(new WvString("HELLO"), true);
198 greeting = blocking_getline(60*1000);
199 if (!greeting || strncmp(greeting,"+OK", 3)) goto fail;
200 // if (!response()) goto fail;
201 log(WvLog::Debug1, "Recv(HELLO): %s", greeting);
203 // log in
204 if (apop_enable && strrchr(greeting, '>'))
206 // APOP login attempt -- early code from fetchmail
207 /* build MD5 digest from greeting timestamp + password */
208 /* find start of timestamp */
209 for (start = greeting; *start != 0 && *start != '<'; start++)
210 continue;
211 if (*start == 0) goto fail;
213 /* find end of timestamp */
214 for (end = start; *end != 0 && *end != '>'; end++)
215 continue;
216 if (*end == 0 || end == start + 1) goto fail;
217 else
218 *++end = '\0';
219 // end of fetchmail code
221 log(WvLog::Debug2, "Using APOP seed: %s\n", start);
223 /* copy timestamp and password into digestion buffer */
224 WvString digestsecret("%s%s",start,password);
225 WvDynBuf md5buf;
226 WvMD5Digest().flushstrbuf(digestsecret, md5buf, true /*finish*/);
227 WvString md5hex = WvHexEncoder().strflushbuf(md5buf, true);
229 log(WvLog::Debug2, "Using APOP response: %s\n", md5hex);
230 cmd("apop %s %s", username, md5hex);
233 if (!apop_enable || !strrchr(greeting, '>')
234 || (!response() && apop_enable_fallback))
236 // USER/PASS login
237 // some providers (i.e.: gmail) get unhappy if you send the user+pass
238 // together, so wait for a response to the user cmd before sending
239 // password.
240 cmd("user %s", username);
241 if (!response()) goto fail;
242 cmd("pass %s", password);
243 if (!response())
245 seterr("Server denied access. Wrong password?");
246 return;
250 // get the number of messages
251 cmd("stat");
252 cmd("list"); // for later
253 if (!response()) goto fail;
254 log(WvLog::Info, "%s %s in %s bytes.\n", res1,
255 res1==1 ? "message" : "messages", res2);
256 nmsgs = res1;
257 mess.set_capacity(nmsgs);
259 // get the list of messages and their sizes
260 if (!response()) goto fail;
261 for (count = 0; count < nmsgs; count++)
263 line = blocking_getline(60*1000);
264 mess.append(new MsgInfo(), true);
265 if (!isok() || !line ||
266 sscanf(line, "%d %ld",
267 &(mess[count]->num), &(mess[count]->len)) != 2)
269 log(WvLog::Error, "Invalid LIST response: '%s'\n", line);
270 goto fail;
273 line = blocking_getline(60*1000);
274 if (!isok() || !line || strcmp(line, ".") && strcmp(line, ".\r"))
276 log(WvLog::Error, "Invalid LIST terminator: '%s'\n", line);
277 goto fail;
280 // sort the list in order of size
281 mess.qsort(messcompare);
283 while (next_ack < nmsgs
284 || (next_ack && mess[next_ack-1]->deletes_after_this)
285 || sendmails || safe_deletes.count() > 0)
287 if (!isok()) break;
288 log(WvLog::Debug4, "next_ack=%s/%s dels=%s sendmails=%s/%s\n",
289 next_ack, nmsgs,
290 mess[0]->deletes_after_this, sendmails,
291 WvSendmailProc::num_sendmails);
293 // do any necessary safe deletes
294 while (safemode && safe_deletes.count() > 0 &&
295 next_req-next_ack < max_requests)
297 cmd(safe_deletes.popstr());
298 if (!response())
300 log(WvLog::Warning, "Failed safe deleting a message!?\n");
301 if (flushing)
302 log(WvLog::Warning, "Canceling future deletions "
303 "to protect the innocent.\n");
304 flushing = false;
305 safe_deletes.zap();
308 if (!isok())
309 goto fail;
312 while (next_req < nmsgs && next_req-next_ack < max_requests)
314 cmd("retr %s", mess[next_req]->num);
315 next_req++;
318 MsgInfo *m = mess[next_ack];
320 while (next_ack > 0 && mess[next_ack-1]->deletes_after_this)
322 if (!response())
324 log(WvLog::Warning, "Failed deleting a message!?\n");
325 if (flushing)
326 log(WvLog::Warning, "Canceling future deletions "
327 "to protect the innocent.\n");
328 flushing = false;
331 mess[next_ack-1]->deletes_after_this--;
333 if (!isok())
334 goto fail;
337 if (next_ack >= nmsgs)
339 never_select = true;
340 continue_select(20);
341 never_select = false;
342 continue; // only deletions and/or sendmails remaining
345 if (!response() && isok())
347 next_ack++;
348 log(WvLog::Warning, "Hmm... missing message #%s.\n", m->num);
349 continue;
352 if (!isok())
354 if (!geterr()) // Keep the older, better?, error message
355 seterr("Aborted.\n");
356 return;
359 next_ack++;
361 WvString size;
362 if (m->len > 10*1024*1024)
363 size = WvString("%sM", m->len/1024/1024);
364 else if (m->len >= 10*1024)
365 size = WvString("%sk", m->len/1024);
366 else if (m->len > 1024)
367 size = WvString("%s.%sk", m->len/1024, (m->len*10/1024) % 10);
368 else
369 size = WvString("%sb", m->len);
371 log(WvLog::Debug1, "%-6s %6s ",
372 WvString("[%s]", mess[next_ack-1]->num), size);
374 from = "";
375 subj = "";
376 printed = false;
377 in_head = true;
378 msgdone = false;
380 while (WvSendmailProc::num_sendmails >= MAX_PROCESSES && isok())
382 never_select = true;
383 continue_select(1000);
384 never_select = false;
388 if (access(mda, X_OK))
390 log(WvLog::Error, "%s: %s\n", mda, strerror(errno));
391 not_found = true;
392 goto fail;
395 const char *argv[] = {mda, deliverto, NULL};
396 //const char *argv[] = {"dd", "of=/dev/null", NULL};
397 WvSendmailProc *p = NULL;
398 if (!explode)
400 p = new WvSendmailProc(argv, next_ack-1,
401 WvSendmailCallback(this, &WvPopClient::send_done));
402 sendmails++;
405 while ((line = blocking_getline(60*1000)) != NULL)
407 if (!isok())
409 if (p)
410 p->kill(SIGTERM);
411 seterr("Connection dropped while reading message contents!");
412 return;
415 // remove \r character, if the server gave one
416 cptr = strchr(line, 0) - 1;
417 if (*cptr == '\r')
418 *cptr = 0;
420 if (line[0]==0 || line[0]=='\r')
421 in_head = false;
422 if (line[0]=='.')
424 if (!strcmp(line, ".") || !strcmp(line, ".\r"))
426 msgdone = true;
427 break; // dot on a line by itself: done this message.
429 else
431 // POP servers turn any line *starting with* a dot into
432 // a line starting with dot-dot; so remove the extra
433 // dot.
434 line++;
438 if (in_head && !strncasecmp(line, "From: ", 6))
440 cptr = strchr(line+6, '<');
441 if (cptr)
443 WvString tmp(cptr + 1);
444 cptr = strchr(tmp.edit(), '>');
445 if (cptr)
446 *cptr = 0;
447 from = WvString("<%s", tmp);
449 else
450 from = line+6;
451 trim_string(from.edit());
453 else if (in_head && explode && !p
454 && !strncasecmp(line, "X-Envelope-To: ", 14))
456 WvString sendto = line+strlen("X-Envelope-To:");
457 sendto.edit();
458 cptr = strchr(sendto, '@');
459 if (cptr)
460 *cptr = 0;
461 trim_string(sendto.edit());
462 const char *argvE[] = {mda, sendto, NULL};
463 p = new WvSendmailProc(argvE, next_ack-1,
464 WvSendmailCallback(this, &WvPopClient::send_done));
465 sendmails++;
467 else if (in_head && !strncasecmp(line, "Subject: ", 9))
469 subj = line+9;
470 trim_string(subj.edit());
473 if (!!from && !!subj && !printed)
475 log(WvLog::Debug1, format, from, subj);
476 printed = true;
479 // ideally we might try to fix the Return-Path here but that
480 // could turn out to be a parsing nightmare so we're going to
481 // just dump it if their POP3 server mangles it
483 if (p && (!in_head || !ignorerp ||
484 strncasecmp(line, "Return-Path: ", 13)))
485 p->print("%s\n", line);
488 if (isok() && !msgdone)
490 if (p)
491 p->kill(SIGTERM);
492 seterr(ETIMEDOUT);
493 return;
496 if (isok() && !printed)
498 if (!subj) subj = "<NO SUBJECT>";
499 if (!from) from = "<NO SENDER";
500 log(WvLog::Debug1, format, from, subj);
503 if (p)
505 p->done();
506 WvIStreamList::globallist.append(p, true, "sendmail");
507 sendprocs.add(p, false);
511 cmd("quit");
512 if (!response()) goto fail;
514 close();
515 return;
517 fail:
518 if (cloned && cloned->geterr())
519 seterr("Server connection error: %s", cloned->errstr());
520 else if (!cloned || !cloned->isok())
521 seterr("Server connection closed unexpectedly (%s bytes left)!",
522 cloned ? inbuf.used() : 0);
523 else if (not_found)
524 seterr("MDA could not be executed!");
525 else
526 seterr("Server said something unexpected!");
527 return;
531 void WvPopClient::send_done(int count, bool success)
533 log(WvLog::Debug4, "send_done %s (success=%s)\n", count+1, success);
535 WvSendmailProc *proc = sendprocs[count];
536 if (proc)
537 sendprocs.remove(proc);
539 sendmails--;
541 if (!success)
543 log(WvLog::Warning, "Error delivering message %s to the MDA.\n",
544 mess[count]->num);
546 else
548 mess[count]->sent = true;
550 // remember that we had one more delete message after the most recent
551 // request (which might have no relation to the message we're deleting,
552 // but we have to count responses carefully!)
553 if (flushing)
555 // in safe mode we do all deletes at the end
556 if (safemode)
558 log(WvLog::Debug3, "Queueing message %s for deletion\n",
559 mess[count]->num);
560 safe_deletes.append(
561 new WvString("dele %s", mess[count]->num), true);
563 else
565 cmd("dele %s", mess[count]->num);
566 mess[next_req-1]->deletes_after_this++;
571 // wake up the WvPopClient, which may have been waiting for a sendmail
572 // to finish.
573 never_select = false;