diff --git a/plugins/archive/archive.pike b/plugins/archive/archive.pike --- a/plugins/archive/archive.pike +++ b/plugins/archive/archive.pike @@ -1,99 +1,100 @@ inherit SpeedyDelivery.Plugin; import Tools.Logging; -constant name = "archive support"; +constant name = "Archive support"; constant description = "support for archived article storage"; int _enabled = 1; mapping checked_exists = ([]); mapping query_event_callers() { return ([ "preDelivery" : archive_message ]); } +// these settings are not yet enabled; configuration is global for the entire list server mapping query_list_settings() { return ([ - "full_text_enabled": (["type": SpeedyDelivery.BOOLEAN, "value": 1, "name": "Enable Full Text"]), - "full_text_authkey": (["type": SpeedyDelivery.STRING, "value": "", "name": "Full Text Auth Key"]) + "full_text_enabled": (["type": Fins.BOOLEAN, "value": 1, "name": "Enable Full Text"]), + "full_text_authkey": (["type": Fins.STRING, "value": "", "name": "Full Text Auth Key"]) ]); } mapping query_destination_callers() { return ([ ]); } int archive_message(string eventname, mapping event, mixed ... args) { Log.info("archiving message"); SpeedyDelivery.Objects.Archived_message m; m = SpeedyDelivery.Objects.Archived_message(); m["List"] = event->list; m["envelope_from"] = (string)event->request->sender; // the envelope_from address almost never includes a nice name. if(event->mime && event->mime->headers->from) { object addr; catch(addr = Mail.MailAddress(event->mime->headers->from)); if(addr) m["envelope_from"] = (string)addr; } m["messageid"] = (string)event->mime->headers["message-id"]; if(event->mime->headers["in-reply-to"]) m["referenceid"] = (string)event->mime->headers["in-reply-to"]; m["subject"] = event->mime->headers->subject; m["content"] = (string)event->mime; m["archived"] = Calendar.dwim_time(event->mime->headers->date); m->save(); updateIndex(eventname, event, m); return SpeedyDelivery.ok; } int updateIndex(string eventname, mapping event, object message) { Thread.Thread(doUpdateIndex, eventname, event, message); return 0; } void doUpdateIndex(string eventname, mapping event, object message) { Log.info("doUpdateIndex"); mapping p = app->config["full_text"]; if(!p) { Log.info("no full text configuration, skipping."); return; } if(!p["url"]) { Log.info("no full text url provided, skipping."); return; } Log.info("getting client: %O, %O.", event, p); string indexname = "SpeedyDelivery_" + event->list["name"]; Log.info("index name: %O", indexname); object c; object e = catch(c = FullText.UpdateClient(p["url"], indexname, p["auth"], 1)); Log.info("got client: %O.", e); Log.info("index ready."); object t = Calendar.dwim_time(event->mime->headers->date); string content; content = Tools.String.textify( SpeedyDelivery.getfullbodytext(event->mime, content)); Log.info("submitting message %O for indexing.", message); c->add(event->mime->headers->subject, t, content, (string)message["id"], Tools.String.make_excerpt(content), "text/mime-message"); } diff --git a/plugins/bounces/bounces.pike b/plugins/bounces/bounces.pike --- a/plugins/bounces/bounces.pike +++ b/plugins/bounces/bounces.pike @@ -1,191 +1,191 @@ inherit SpeedyDelivery.Plugin; import Tools.Logging; -constant name = "bounce support"; +constant name = "Bounce support"; constant description = "support for handling bounces"; int _enabled = 1; /* some regular expressions we've shamelessly borrowed from smartlist's etc/rc.request */ constant transient_bounce_subject = "(Warning - delayed mail|(WARNING: message ([^ ]+ )?|Mail )delayed|" "(Returned mail: )?(warning: c(an|ould )not send m(essage fo|ail afte)r|Unbalanced '\"'|" "Cannot send (within [0-9]|8-bit data to 7-bit)|" "Data format error|Headers too large|Eight bit data not allowed|" "Message (size )?exceeds (fixed )?maximum (fixed|message) size)|" "Undeliverable (RFC822 )?mail: temporarily unable to deliver|" "\\*\\*\\* WARNING - Undelivered mail in mailqueue|Execution succee?ded)"; constant warning_subject = "(Warning from|mail warning| ?Waiting mail)"; constant warning_senders = ".*(uucp|mmdf)"; constant warning_removed = "You have been removed from"; constant x_loop_bounce = "\(bounce\)"; mapping query_destination_callers() { return (["bounces": handle_bounce]); } int handle_bounce(SpeedyDelivery.Request r) { Log.info("the following bounce was received: %O", r->mime->headers); array bouncers; int handled; if(is_bounce(r)) { bouncers = ({ extract_bouncer(r) }); } else if(is_dsn(r)) { bouncers = extract_bouncer_from_dsn(r); // werror("bouncers: %O\n", bouncers); } // we shouldn't reuse this array, really. bouncers = Fins.Model.find.subscribers((["email": Fins.Model.InCriteria(bouncers)])); werror("bouncers: %O\n", bouncers); // if we have more than hit, it's probably ambiguous. if(sizeof(bouncers) == 1) { bouncers->has_bounced(r->list); handled++; } if(!handled) { // else // misdirected messages should go to the list owner. string subject = "Unhandled bounce message for " + r->list["name"]; string message = "A message was sent to this list's bounce address,\n " "however we weren't able to identify it as a bounce. \n" "Please examine it and if necessary, manually unsubscribe\n" "the user in question.\n\n"; app->send_message_as_attachment_to_list_owner(r->list, subject, message, r->mime); } return 0; } string extract_bouncer(SpeedyDelivery.Request r) { string bouncer; string u,h; string d = r->mime->getdata(); int matches; Log.debug("extracting bouncer."); do { matches = sscanf(d, "%*s %s@%s[ \\r\\n]%s", u, h, d); if(matches >= 3) Log.debug("got an address: %s@%s\n", u, h); } while(d && sizeof(d) && matches == 4); return bouncer; } array extract_bouncer_from_dsn(SpeedyDelivery.Request r) { array bouncers = ({}); string d = SpeedyDelivery.getfullbodymimetext(r->mime, "message/delivery-status"); Log.debug("extracting bouncer from dsn."); object rx = Regexp.SimpleRegexp(".*[ \t\r\n\<;](.*@.*)[ \\t\\r\\n\\>].*"); foreach(d/"\n";; string l) { array x = rx->split(l); if(x) { bouncers += ({x[0]}); } } return Array.uniq(bouncers); } int is_dsn(SpeedyDelivery.Request r) { if(has_prefix(r->mime->headers["content-type"], "multipart/report")) { Log.debug("match on multipart_report"); return 1; } if(r->mime->body_parts) { foreach(r->mime->body_parts;; object bp) { if(has_prefix(bp->headers["content-type"], "multipart/report")) { Log.debug("match on subpart multipart_report"); return 1; } if(has_prefix(bp->headers["content-type"], "message/delivery-status")) { Log.debug("match on subpart delivery_status"); return 1; } } } return 0; } int is_bounce(SpeedyDelivery.Request r) { object regexp = Regexp.SimpleRegexp(transient_bounce_subject); if(regexp->match(r->mime->headers["subject"])) { Log.debug("match on transient_bounce_subject"); return 1; } regexp = Regexp.SimpleRegexp(warning_senders); if(regexp->match(r->mime->headers["from"])) { Log.debug("match on warning_senders"); return 1; } if(regexp->match(r->mime->headers["sender"]||"")) { Log.debug("match on warning_senders"); return 1; } regexp = Regexp.SimpleRegexp(warning_subject); if(regexp->match(r->mime->headers["subject"])) { Log.debug("match on warning_subject"); return 1; } regexp = Regexp.SimpleRegexp(warning_removed); if(regexp->match(r->mime->headers["subject"])) { Log.debug("match on warning_removed"); return 1; } regexp = Regexp.SimpleRegexp(x_loop_bounce); if(regexp->match(r->mime->headers["x-loop"]||"")) { Log.debug("match on x_loop_bounce"); return 1; } return 0; } diff --git a/plugins/digester/digester.pike b/plugins/digester/digester.pike --- a/plugins/digester/digester.pike +++ b/plugins/digester/digester.pike @@ -1,95 +1,95 @@ inherit SpeedyDelivery.Plugin; import Tools.Logging; -constant name = "digest delivery support"; +constant name = "Digest delivery support"; constant description = "support for digest delivery"; int _enabled = 1; int checked_exists = 0; mapping query_event_callers() { return ([ "postDelivery" : check_digest_ready ]); } /* mapping query_destination_callers() { return ([ ]); } */ void start() { call_out(schedule_process_digests, 5); } void schedule_process_digests() { // TODO: make sure we only have one running at a time! mixed e = catch(process_digests()); if(e) Log.exception("An exception occurred while processing digests.\n", e); call_out(schedule_process_digests, 3600*6); } // check to see if we have 10 emails ready to send; our default max digest // size. triggered after each email, so we can be sure that the digest // for any list won't grow too big. int check_digest_ready(string eventname, mapping event) { int targetsize = event->list["_options"]["digest_size"] || 10; array items = Fins.Model.find.archived_messages((["List": event->list, "digested": 0])); mixed e; // if the item count is less than the digest size or the total size of messages is less than 10mb, let it ride. if(!items || ((sizeof(items) < targetsize) && outbound_size(items) < 10*1024*1024)) return SpeedyDelivery.ok; e = catch(process_digest(event->list, items)); if(e) Log.exception("An error occurred while digesting list " + event->list["name"] + ".\n", e); return SpeedyDelivery.ok; } int outbound_size(array items) { int obs = 0; if(!items) return 0; foreach(items;; object item) obs += sizeof(item["content"]); return obs; } void process_digests() { Log.info("Preparing digests for all lists."); foreach(Fins.Model.find.lists_all();; SpeedyDelivery.Objects.List l) { // only process digests for those lists who haven't had a digest in // the last day. int q = l["_options"]["last_digested"]; if(!q || (time()-q) >= (3600*24)) process_digest(l); } } // generate a mime digest for list l, marking any items as digested // saves the time of the digesting for digest spacing purposes. void process_digest(SpeedyDelivery.Objects.List l, void|array items) { Log.info("Generating Digest for " + l["name"]); if(!items) items = Fins.Model.find.archived_messages((["List": l, "digested": 0])); l["_options"]["last_digested"] = time(); if(!sizeof(items)) return; object digest = SpeedyDelivery.generate_digest_message(l, items); foreach(items;;mixed item) item["digested"] = 1; app->send_message_for_list( l, Fins.Model.find.subscriptions((["List": l, "mode": "D"]))[*]["Subscriber"][*]["email"], (string)digest); } diff --git a/plugins/list_subject/list_subject.pike b/plugins/list_subject/list_subject.pike --- a/plugins/list_subject/list_subject.pike +++ b/plugins/list_subject/list_subject.pike @@ -1,29 +1,29 @@ inherit SpeedyDelivery.Plugin; import Tools.Logging; -constant name = "list name in subject"; +constant name = "List name in subject"; constant description = "inserts the list name into the subject of a list"; constant list_enableable = 1; int _enabled = 1; mapping query_event_callers() { return ([ "rewriteMessage" : rewrite_subject ]); } mapping query_destination_callers() { return ([ ]); } int rewrite_subject(string eventname, mapping event, mixed ... args) { Log.debug("rewriting subject for message: " + event->mime->subject); string s = "[" + event->request->list["name"] + "]"; if(search(event->mime->headers->subject, s) == -1) event->mime->headers->subject = s + " " + event->mime->headers->subject; return SpeedyDelivery.ok; } diff --git a/plugins/moderate/moderate.pike b/plugins/moderate/moderate.pike --- a/plugins/moderate/moderate.pike +++ b/plugins/moderate/moderate.pike @@ -1,146 +1,146 @@ inherit SpeedyDelivery.Plugin; import Tools.Logging; -constant name = "moderation support"; +constant name = "Moderation support"; constant description = "support for moderating list activity"; int _enabled = 1; int checked_exists = 0; mapping query_event_callers() { return ([ "holdMessage" : hold_message ]); } mapping query_destination_callers() { return ([ "moderate": handle_release ]); } int hold_message(string eventname, mapping event, mixed ... args) { Log.info("generating hold message for %s\n", event->list["name"]); object mime = MIME.Message(); mime->headers["subject"] = "Moderation request from " + event->hold["envelope_from"] + " to " + event->list["name"]; mime->headers["from"] = app->get_address_for_function(event->list, "moderate"); mime->headers["to"] = app->get_owner_addresses(event->list); string msg = event->list["_options"]["moderate_message"] || #string "moderate.txt"; object v = app->view->get_string_view(msg); v->add("list", event->list); v->add("hold", event->hold); mime->setdata(v->render()); app->send_message_to_list_owner(event->list, (string)mime); return SpeedyDelivery.ok; } int handle_release(SpeedyDelivery.Request r) { string s = r->mime->headers->subject + " " + SpeedyDelivery.getfullbodytext(r->mime); // format of the confirmation message is: // (space)CONFIRM(space)list-name(space)confirmcode(whitespace) // where confirmcode is a 25 character hex hash. string ln, hc; if(sscanf(s, "%*s RELEASE %s %25[0-9a-f]", ln, hc) == 3) { Log.info("have a correctly formed release response."); if(ln == r->list["name"]) return release_message(r, ln, hc); } else if(sscanf(s, "%*s REJECT %s %25[0-9a-f]", ln, hc) == 3) { Log.info("have a correctly formed reject response."); if(ln == r->list["name"]) return reject_message(r, ln, hc); } return SpeedyDelivery.ok; } int release_message(SpeedyDelivery.Request r, string ln, string hc) { Log.info("handling release id %s for list %s.", hc, ln); if(r->list["name"] != ln) return 0; Log.info("=> finding release id %s for list %s.", hc, ln); object x; catch( x = Fins.Model.find.held_messages_by_alt(hc)); SpeedyDelivery.Objects.Held_message c; c = [object(SpeedyDelivery.Objects.Held_message)]x; if(!c) return 1; Log.info("=> found held message."); // if(c["conftype"] != r->functionname) return 0; if(c["List"]["name"] != r->list["name"]) return 0; else { Log.info("=> releasing id %s for list %s.", hc, ln); c->release(); object mime = MIME.Message(); mime->headers["subject"] = "Moderation result for " + r->list["name"]; mime->headers["from"] = app->get_address_for_function(r->list, "moderate"); mime->headers["to"] = (string)r->sender; string msg = r->list["_options"]["moderate_response"] || #string "response.txt"; object v = app->view->get_string_view(msg); v->add("list", r->list); v->add("holdid", hc); v->add("request", "release"); mime->setdata(v->render()); app->send_message(mime->headers["from"], (string)r->sender, (string)mime); return 0; } } int reject_message(SpeedyDelivery.Request r, string ln, string hc) { Log.info("handling reject id %s for list %s.", hc, ln); if(r->list["name"] != ln) return 0; Log.info("=> finding reject id %s for list %s.", hc, ln); object x; catch( x = Fins.Model.find.held_messages_by_alt(hc)); SpeedyDelivery.Objects.Held_message c; c = [object(SpeedyDelivery.Objects.Held_message)]x; if(!c) return 1; // if(c["conftype"] != r->functionname) return 0; if(c["List"]["name"] != r->list["name"]) return 0; else { Log.info("=> rejecting id %s for list %s.", hc, ln); c->delete(); object mime = MIME.Message(); mime->headers["subject"] = "Moderation result for " + r->list["name"]; mime->headers["from"] = app->get_address_for_function(r->list, "moderate"); mime->headers["to"] = (string)r->sender; string msg = r->list["_options"]["moderate_response"] || #string "response.txt"; object v = app->view->get_string_view(msg); v->add("list", r->list); v->add("holdid", hc); v->add("request", "reject"); mime->setdata(v->render()); app->send_message(mime->headers["from"], (string)r->sender, (string)mime); return 0; } } diff --git a/plugins/owner/owner.pike b/plugins/owner/owner.pike --- a/plugins/owner/owner.pike +++ b/plugins/owner/owner.pike @@ -1,19 +1,19 @@ inherit SpeedyDelivery.Plugin; import Tools.Logging; -constant name = "owner support"; +constant name = "Owner support"; constant description = "list owner email address"; int _enabled = 1; mapping query_destination_callers() { return (["owner": handle_owner]); } int handle_owner(SpeedyDelivery.Request r) { app->send_message_to_list_owner(r->list, (string)(r->mime)); return 250; } diff --git a/plugins/subscribe/subscribe.pike b/plugins/subscribe/subscribe.pike --- a/plugins/subscribe/subscribe.pike +++ b/plugins/subscribe/subscribe.pike @@ -1,116 +1,116 @@ inherit SpeedyDelivery.Plugin; import Tools.Logging; -constant name = "subscribe support"; +constant name = "Subscribe support"; constant description = "support for subscription via email"; int _enabled = 1; mapping query_event_callers() { return (["postSubscribe": after_subscribe, "createNewSubscriber": new_subscriber ]); } mapping query_destination_callers() { return (["subscribe": handle_subscribe]); } int handle_subscribe(SpeedyDelivery.Request r) { string s = r->mime->headers->subject + " " + SpeedyDelivery.getfullbodytext(r->mime); // format of the confirmation message is: // (space)CONFIRM(space)list-name(space)confirmcode(whitespace) // where confirmcode is a 25 character hex hash. string ln, hc; if(sscanf(s, "%*s CONFIRM %s %25[0-9a-f]", ln, hc) == 3) { Log.info("have a correctly formed confirmation response."); if(ln == r->list["name"]) return confirm_subscription(r, ln, hc); } array sa = replace(lower_case(Tools.String.textify(s)), ({"\r", "\n", "\t", ".", ",", "!", "?"}), ({" ", " ", " ", " ", " ", " ", " "}))/" "; if(search(sa, "subscribe") == -1) // we don't have the magic word! { Log.info("sending help to wandering subscriber."); return app->generate_help(r, "subscribe"); } return r->list->request_subscription(r->sender); } int new_subscriber(string eventname, mapping event, mixed ... args) { object mime = MIME.Message(); mime->headers["subject"] = "Welcome to SpeedyDelivery!"; mime->headers["to"] = event->subscriber->get_address(); mime->headers["from"] = app->get_listmaster_address(); string msg = #string "new_user.txt"; object v = app->view->get_string_view(msg); v->add("user", event->subscriber); v->add("password", event->password); mime->setdata(v->render()); Log.info("sending welcome subscriber email to %s.", event->subscriber->get_address()); app->send_message( app->get_listmaster_address(), ({event->subscriber->get_address()}), (string)mime); } int after_subscribe(string eventname, mapping event, mixed ... args) { if(!event->quiet) { object mime = MIME.Message(); mime->headers["subject"] = "Welcome to " + event->list["name"]; mime->headers["to"] = event->subscriber->get_address(); mime->headers["from"] = app->get_bounce_address(event->list); string msg = event->list["_options"]["welcome_message"] || #string "welcome.txt"; object v = app->view->get_string_view(msg); v->add("list", event->list); v->add("subscriber", event->subscriber); mime->setdata(v->render()); app->send_message_for_list(event->list, ({event->subscriber->get_address()}), (string)mime); } } int confirm_subscription(SpeedyDelivery.Request r, string ln, string hc) { Log.info("handling confirmation id %s for list %s.", hc, ln); if(r->list["name"] != ln) return 0; Log.info("=> handling confirmation id %s for list %s.", hc, ln); object x; catch( x = Fins.Model.find.confirmations_by_alt(hc)); SpeedyDelivery.Objects.Confirmation c; c = [object(SpeedyDelivery.Objects.Confirmation)]x; Log.info("handling confirmation %O for list %s.", x, r->list["name"]); if(!c) return 1; if(c["conftype"] != r->functionname) return 0; if(c["list"] != r->list["name"]) return 0; else { r->list->subscribe(c); return 0; } } diff --git a/plugins/unsubscribe/unsubscribe.pike b/plugins/unsubscribe/unsubscribe.pike --- a/plugins/unsubscribe/unsubscribe.pike +++ b/plugins/unsubscribe/unsubscribe.pike @@ -1,109 +1,109 @@ inherit SpeedyDelivery.Plugin; import Tools.Logging; -constant name = "unsubscribe support"; +constant name = "Unsubscribe support"; constant description = "support for unsubscription via email"; int _enabled = 1; mapping query_event_callers() { return (["postUnsubscribe": after_unsubscribe, ]); } mapping query_destination_callers() { return (["unsubscribe": handle_unsubscribe]); } string getfullbodytext(object mime, string|void s) { if(!s) s = ""; s = mime->getdata(); if(mime->body_parts) { foreach(mime->body_parts;; object nm) s = getfullbodytext(nm, s); } return s; } int handle_unsubscribe(SpeedyDelivery.Request r) { string s = r->mime->headers->subject + " " + getfullbodytext(r->mime); // format of the confirmation message is: // (space)CONFIRM(space)list-name(space)confirmcode(whitespace) // where confirmcode is a 25 character hex hash. string ln, hc; Log.info("handling unsubscribe message from %O.", r->sender); if(sscanf(s, "%*s CONFIRM %s %25[0-9a-f]", ln, hc) == 3) { Log.info("have a correctly formed confirmation response."); if(ln == r->list["name"]) return confirm_unsubscription(r, ln, hc); } if(search(lower_case(Tools.String.textify(s)), "unsubscribe") == -1) // we don't have the magic word! { Log.info("sending help to wandering unsubscriber."); return app->generate_help(r, "unsubscribe"); } return r->list->request_unsubscription(r->sender); } int after_unsubscribe(string eventname, mapping event, mixed ... args) { if(!event->quiet) { object mime = MIME.Message(); mime->headers["subject"] = "Successful Unsubscription from " + event->list["name"]; mime->headers["to"] = event->subscriber->get_address(); mime->headers["from"] = app->get_bounce_address(event->list); // TODO: we should use standard Fins templates rather than // our own crazy substitution system. string msg = event->list["_options"]["goodbye_message"] || #string "goodbye.txt"; // vals["#list.posting_address#"] = app->get_address_for_function(event->list, 0); // vals["#list.unsubscribe_address#"] = app->get_address_for_function(event->list, "unsubscribe"); object v = app->view->get_string_view(msg); v->add("list", event->list); v->add("subscriber", event->subscriber); mime->setdata(v->render()); app->send_message_for_list(event->list, ({event->subscriber->get_address()}), (string)mime); } } int confirm_unsubscription(SpeedyDelivery.Request r, string ln, string hc) { Log.info("handling confirmation id %s for list %s.", hc, ln); if(r->list["name"] != ln) return 0; object x; SpeedyDelivery.Objects.Confirmation c; catch( x = Fins.Model.find.confirmations_by_alt(hc)); c = [object(SpeedyDelivery.Objects.Confirmation)]x; if(!c) return 1; if(c["conftype"] != r->functionname) return 0; if(c["list"] != r->list["name"]) return 0; else { Log.info("Unsubscribing %O from %O.", r->sender, r->list["name"]); r->list->unsubscribe(c); return 0; } }