andrei@versabanq.com: wvdbusd: handle *all* messages addressed to o.f.D.
[wvapps.git] / retchmail / wvpopclient.cc
blob12cce5e89970dcd8de7040c884c04184e1507be9
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 bool messcompare(const WvPopClient::MsgInfo &a,
154 const WvPopClient::MsgInfo &b)
156 const long base = 10240;
157 long alen = a.len / base, blen = b.len / base;
158 return ((alen * base + a.num) - (blen * base + b.num)) < 0;
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.reserve(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.push_back(MsgInfo());
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 std::sort(mess.begin(), mess.end(), 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 wv::bind(&WvPopClient::send_done, this,
402 _1, _2));
403 sendmails++;
406 while ((line = blocking_getline(60*1000)) != NULL)
408 if (!isok())
410 if (p)
411 p->kill(SIGTERM);
412 seterr("Connection dropped while reading message contents!");
413 return;
416 // remove \r character, if the server gave one
417 cptr = strchr(line, 0) - 1;
418 if (*cptr == '\r')
419 *cptr = 0;
421 if (line[0]==0 || line[0]=='\r')
422 in_head = false;
423 if (line[0]=='.')
425 if (!strcmp(line, ".") || !strcmp(line, ".\r"))
427 msgdone = true;
428 break; // dot on a line by itself: done this message.
430 else
432 // POP servers turn any line *starting with* a dot into
433 // a line starting with dot-dot; so remove the extra
434 // dot.
435 line++;
439 if (in_head && !strncasecmp(line, "From: ", 6))
441 cptr = strchr(line+6, '<');
442 if (cptr)
444 WvString tmp(cptr + 1);
445 cptr = strchr(tmp.edit(), '>');
446 if (cptr)
447 *cptr = 0;
448 from = WvString("<%s", tmp);
450 else
451 from = line+6;
452 trim_string(from.edit());
454 else if (in_head && explode && !p
455 && !strncasecmp(line, "X-Envelope-To: ", 14))
457 WvString sendto = line+strlen("X-Envelope-To:");
458 sendto.edit();
459 cptr = strchr(sendto, '@');
460 if (cptr)
461 *cptr = 0;
462 trim_string(sendto.edit());
463 const char *argvE[] = {mda, sendto, NULL};
464 p = new WvSendmailProc(argvE, next_ack-1,
465 wv::bind(&WvPopClient::send_done, this,
466 _1, _2));
467 sendmails++;
469 else if (in_head && !strncasecmp(line, "Subject: ", 9))
471 subj = line+9;
472 trim_string(subj.edit());
475 if (!!from && !!subj && !printed)
477 log(WvLog::Debug1, format, from, subj);
478 printed = true;
481 // ideally we might try to fix the Return-Path here but that
482 // could turn out to be a parsing nightmare so we're going to
483 // just dump it if their POP3 server mangles it
485 if (p && (!in_head || !ignorerp ||
486 strncasecmp(line, "Return-Path: ", 13)))
487 p->print("%s\n", line);
490 if (isok() && !msgdone)
492 if (p)
493 p->kill(SIGTERM);
494 seterr(ETIMEDOUT);
495 return;
498 if (isok() && !printed)
500 if (!subj) subj = "<NO SUBJECT>";
501 if (!from) from = "<NO SENDER";
502 log(WvLog::Debug1, format, from, subj);
505 if (p)
507 p->done();
508 WvIStreamList::globallist.append(p, true, "sendmail");
509 sendprocs.add(p, false);
513 cmd("quit");
514 if (!response()) goto fail;
516 close();
517 return;
519 fail:
520 if (cloned && cloned->geterr())
521 seterr("Server connection error: %s", cloned->errstr());
522 else if (!cloned || !cloned->isok())
523 seterr("Server connection closed unexpectedly (%s bytes left)!",
524 cloned ? inbuf.used() : 0);
525 else if (not_found)
526 seterr("MDA could not be executed!");
527 else
528 seterr("Server said something unexpected!");
529 return;
533 void WvPopClient::send_done(int count, bool success)
535 log(WvLog::Debug4, "send_done %s (success=%s)\n", count+1, success);
537 WvSendmailProc *proc = sendprocs[count];
538 if (proc)
539 sendprocs.remove(proc);
541 sendmails--;
543 if (!success)
545 log(WvLog::Warning, "Error delivering message %s to the MDA.\n",
546 mess[count].num);
548 else
550 mess[count].sent = true;
552 // remember that we had one more delete message after the most recent
553 // request (which might have no relation to the message we're deleting,
554 // but we have to count responses carefully!)
555 if (flushing)
557 // in safe mode we do all deletes at the end
558 if (safemode)
560 log(WvLog::Debug3, "Queueing message %s for deletion\n",
561 mess[count].num);
562 safe_deletes.append(
563 new WvString("dele %s", mess[count].num), true);
565 else
567 cmd("dele %s", mess[count].num);
568 mess[next_req-1].deletes_after_this++;
573 // wake up the WvPopClient, which may have been waiting for a sendmail
574 // to finish.
575 never_select = false;