/* ** Copyright (c) 2007 D. Richard Hipp ** ** This program is free software; you can redistribute it and/or ** modify it under the terms of the Simplified BSD License (also ** known as the "2-Clause License" or "FreeBSD License".) ** ** This program is distributed in the hope that it will be useful, ** but without any warranty; without even the implied warranty of ** merchantability or fitness for a particular purpose. ** ** Author contact information: ** drh@hwaci.com ** http://www.hwaci.com/drh/ ** ******************************************************************************* ** ** Email notification features */ #include "config.h" #include "email.h" #include /* ** SQL code to implement the tables needed by the email notification ** system. */ static const char zEmailInit[] = @ -- Subscribers are distinct from users. A person can have a log-in in @ -- the USER table without being a subscriber. Or a person can be a @ -- subscriber without having a USER table entry. Or they can have both. @ -- In the last case the suname column points from the subscriber entry @ -- to the USER entry. @ -- @ -- The ssub field is a string where each character indicates a particular @ -- type of event to subscribe to. Choices: @ -- a - Announcements @ -- c - Check-ins @ -- t - Ticket changes @ -- w - Wiki changes @ -- Probably different codes will be added in the future. In the future @ -- we might also add a separate table that allows subscribing to email @ -- notifications for specific branches or tags or tickets. @ -- @ CREATE TABLE repository.subscriber( @ subscriberId INTEGER PRIMARY KEY, -- numeric subscriber ID. Internal use @ subscriberCode TEXT UNIQUE, -- UUID for subscriber. External use @ sname TEXT, -- Human readable name @ suname TEXT, -- Corresponding USER or NULL @ semail TEXT, -- email address @ sverify BOOLEAN, -- email address verified @ sdonotcall BOOLEAN, -- true for Do Not Call @ sdigest BOOLEAN, -- true for daily digests only @ ssub TEXT, -- baseline subscriptions @ sctime DATE, -- When this entry was created. JulianDay @ smtime DATE, -- Last change. JulianDay @ sipaddr TEXT, -- IP address for last change @ spswdHash TEXT -- SHA3 hash of password @ ); @ @ -- Email notifications that need to be sent. @ -- @ -- If the eventid key is an integer, then it corresponds to the @ -- EVENT.OBJID table. Other kinds of eventids are reserved for @ -- future expansion. @ -- @ CREATE TABLE repository.email_pending( @ eventid ANY PRIMARY KEY, -- Object that changed @ sentSep BOOLEAN DEFAULT false, -- individual emails sent @ sentDigest BOOLEAN DEFAULT false -- digest emails sent @ ) WITHOUT ROWID; @ @ -- Record bounced emails. If too many bounces are received within @ -- some defined time range, then cancel the subscription. Older @ -- entries are periodically purged. @ -- @ CREATE TABLE repository.email_bounce( @ subscriberId INTEGER, -- to whom the email was sent. @ sendTime INTEGER, -- seconds since 1970 when email was sent @ rcvdTime INTEGER -- seconds since 1970 when bounce was received @ ); ; /* ** Make sure the unversioned table exists in the repository. */ void email_schema(void){ if( !db_table_exists("repository", "subscriber") ){ db_multi_exec(zEmailInit/*works-like:""*/); } } /* ** WEBPAGE: setup_email ** ** Administrative page for configuring and controlling email notification */ void setup_email(void){ static const char *const azSendMethods[] = { "off", "Disabled", "pipe", "Pipe to a command", "db", "Store in a database", "file", "Store in a directory" }; login_check_credentials(); if( !g.perm.Setup ){ login_needed(0); return; } db_begin_transaction(); style_header("Email Notification Setup"); @
@
login_insert_csrf_secret(); multiple_choice_attribute("Email Send Method","email-send-method", "esm", "off", count(azSendMethods)/2, azSendMethods); @

How to send email. The "Pipe to a command" @ method is the usual choice in production. @ (Property: "email-send-method")

@
entry_attribute("Command To Pipe Email To", 80, "esc", "email-send-command", "sendmail -t", 0); @

When the send method is "pipe to a command", this is the command @ that is run. Email messages are piped into the standard input of this @ command. The command is expected to extract the sender address, @ recepient addresses, and subject from the header of the piped email @ text. (Property: "email-send-command")

entry_attribute("Database In Which To Store Email", 60, "esdb", "email-send-db", "", 0); @

When the send method is "store in a databaes", each email message is @ stored in an SQLite database file with the name given here. @ (Property: "email-send-db")

entry_attribute("Directory In Which To Store Email", 60, "esdir", "email-send-dir", "", 0); @

When the send method is "store in a directory", each email message is @ stored as a separate file in the directory shown here. @ (Property: "email-send-dir")

@
entry_attribute("\"From\" email address", 40, "ef", "email-self", "", 0); @

This is the email from which email notifications are sent. The @ system administrator should arrange for emails sent to this address @ to be handed off to the "fossil email incoming" command so that Fossil @ can handle bounces. (Property: "email-self")

@
entry_attribute("Administrator email address", 40, "ea", "email-admin", "", 0); @

This is the email for the human administrator for the system. @ Abuse and trouble reports are send here. @ (Property: "email-admin")

@
@

@
db_end_transaction(0); style_footer(); } /* ** Encode pMsg as MIME base64 and append it to pOut */ static void append_base64(Blob *pOut, Blob *pMsg){ int n, i, k; char zBuf[100]; n = blob_size(pMsg); for(i=0; i=3 ? g.argv[2] : "x"; nCmd = (int)strlen(zCmd); if( strncmp(zCmd, "reset", nCmd)==0 ){ Blob yn; int c; fossil_print( "This will erase all content in the repository tables, thus\n" "deleting all subscriber information. The information will be\n" "unrecoverable.\n"); prompt_user("Continue? (y/N) ", &yn); c = blob_str(&yn)[0]; if( c=='y' ){ db_multi_exec( "DROP TABLE IF EXISTS subscriber;\n" "DROP TABLE IF EXISTS subscription;\n" "DROP TABLE IF EXISTS email_pending;\n" "DROP TABLE IF EXISTS email_bounce;\n" ); email_schema(); } blob_zero(&yn); }else if( strncmp(zCmd, "send", nCmd)==0 ){ Blob prompt, body, hdr; int sendAsBoth = find_option("both",0,0)!=0; int sendAsHtml = find_option("html",0,0)!=0; const char *zDest = find_option("stdout",0,0)!=0 ? "stdout" : 0; int i; const char *zSubject = find_option("subject", "S", 1); const char *zSource = find_option("body", 0, 1); verify_all_options(); blob_init(&prompt, 0, 0); blob_init(&body, 0, 0); blob_init(&hdr, 0, 0); for(i=3; i\n%h\n", blob_str(&body)); email_send(&hdr, &body, &html, zDest); blob_zero(&html); }else{ email_send(&hdr, &body, 0, zDest); } blob_zero(&hdr); blob_zero(&body); blob_zero(&prompt); } else if( strncmp(zCmd, "settings", nCmd)==0 ){ int isGlobal = find_option("global",0,0)!=0; int i; int nSetting; const Setting *pSetting = setting_info(&nSetting); db_open_config(1, 0); verify_all_options(); if( g.argc!=3 && g.argc!=5 ) usage("setting [NAME VALUE]"); if( g.argc==5 ){ const char *zLabel = g.argv[3]; if( strncmp(zLabel, "email-", 6)!=0 || (pSetting = db_find_setting(zLabel, 1))==0 ){ fossil_fatal("not a valid email setting: \"%s\"", zLabel); } db_set(pSetting->name, g.argv[4], isGlobal); g.argc = 3; } pSetting = setting_info(&nSetting); for(; nSetting>0; nSetting--, pSetting++ ){ if( strncmp(pSetting->name,"email-",6)!=0 ) continue; print_setting(pSetting); } } else{ usage("reset|send|setting"); } } /* ** WEBPAGE: subscribe ** ** Allow users to subscribe to email notifications, or to change or ** verify their subscription. */ void subscribe_page(void){ int needCaptcha; unsigned int uSeed; const char *zDecoded; char *zCaptcha; login_check_credentials(); if( !g.perm.EmailAlert ){ login_needed(g.anon.EmailAlert); return; } style_header("Email Subscription"); needCaptcha = P("usecaptcha")!=0 || login_is_nobody() || login_is_special(g.zLogin); form_begin(0, "%R/subscribe"); @ @ @ @ @ @ if( needCaptcha ){ uSeed = captcha_seed(); zDecoded = captcha_decode(uSeed); zCaptcha = captcha_render(zDecoded); @ @ @ @ @ @ } @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ if( needCaptcha ){ @
    @ %h(zCaptcha)
    @ 
@ Enter the 8 characters above in the "Security Code" box @
} @ style_footer(); }