Fossil SCM

fossil-scm / src / alerts.c
Source Blame History 3686 lines
fc5c7d2… drh 1 /*
fc5c7d2… drh 2 ** Copyright (c) 2018 D. Richard Hipp
fc5c7d2… drh 3 **
fc5c7d2… drh 4 ** This program is free software; you can redistribute it and/or
fc5c7d2… drh 5 ** modify it under the terms of the Simplified BSD License (also
fc5c7d2… drh 6 ** known as the "2-Clause License" or "FreeBSD License".)
fc5c7d2… drh 7 **
fc5c7d2… drh 8 ** This program is distributed in the hope that it will be useful,
fc5c7d2… drh 9 ** but without any warranty; without even the implied warranty of
fc5c7d2… drh 10 ** merchantability or fitness for a particular purpose.
fc5c7d2… drh 11 **
fc5c7d2… drh 12 ** Author contact information:
fc5c7d2… drh 13 ** [email protected]
fc5c7d2… drh 14 ** http://www.hwaci.com/drh/
fc5c7d2… drh 15 **
fc5c7d2… drh 16 *******************************************************************************
fc5c7d2… drh 17 **
5d28db4… drh 18 ** Logic for email notification, also known as "alerts" or "subscriptions".
fc5c7d2… drh 19 **
fc5c7d2… drh 20 ** Are you looking for the code that reads and writes the internet
fc5c7d2… drh 21 ** email protocol? That is not here. See the "smtp.c" file instead.
fc5c7d2… drh 22 ** Yes, the choice of source code filenames is not the greatest, but
fc5c7d2… drh 23 ** it is not so bad that changing them seems justified.
e7a5b98… stephan 24 */
fc5c7d2… drh 25 #include "config.h"
fc5c7d2… drh 26 #include "alerts.h"
fc5c7d2… drh 27 #include <assert.h>
fc5c7d2… drh 28 #include <time.h>
fc5c7d2… drh 29
fc5c7d2… drh 30 /*
fc5c7d2… drh 31 ** Maximum size of the subscriberCode blob, in bytes
fc5c7d2… drh 32 */
fc5c7d2… drh 33 #define SUBSCRIBER_CODE_SZ 32
fc5c7d2… drh 34
fc5c7d2… drh 35 /*
fc5c7d2… drh 36 ** SQL code to implement the tables needed by the email notification
fc5c7d2… drh 37 ** system.
fc5c7d2… drh 38 */
fc5c7d2… drh 39 static const char zAlertInit[] =
fc5c7d2… drh 40 @ DROP TABLE IF EXISTS repository.subscriber;
fc5c7d2… drh 41 @ -- Subscribers are distinct from users. A person can have a log-in in
fc5c7d2… drh 42 @ -- the USER table without being a subscriber. Or a person can be a
fc5c7d2… drh 43 @ -- subscriber without having a USER table entry. Or they can have both.
fc5c7d2… drh 44 @ -- In the last case the suname column points from the subscriber entry
fc5c7d2… drh 45 @ -- to the USER entry.
fc5c7d2… drh 46 @ --
fc5c7d2… drh 47 @ -- The ssub field is a string where each character indicates a particular
fc5c7d2… drh 48 @ -- type of event to subscribe to. Choices:
fc5c7d2… drh 49 @ -- a - Announcements
fc5c7d2… drh 50 @ -- c - Check-ins
60d40d5… drh 51 @ -- f - Forum posts
bca95cb… drh 52 @ -- k - ** Special: Unsubscribed using /oneclickunsub
d4361f6… drh 53 @ -- n - New forum threads
d4361f6… drh 54 @ -- r - Replies to my own forum posts
fc5c7d2… drh 55 @ -- t - Ticket changes
36f72c0… stephan 56 @ -- u - Changes of users' permissions (admins only)
fc5c7d2… drh 57 @ -- w - Wiki changes
d4361f6… drh 58 @ -- x - Edits to forum posts
fc5c7d2… drh 59 @ -- Probably different codes will be added in the future. In the future
fc5c7d2… drh 60 @ -- we might also add a separate table that allows subscribing to email
fc5c7d2… drh 61 @ -- notifications for specific branches or tags or tickets.
fc5c7d2… drh 62 @ --
fc5c7d2… drh 63 @ CREATE TABLE repository.subscriber(
fc5c7d2… drh 64 @ subscriberId INTEGER PRIMARY KEY, -- numeric subscriber ID. Internal use
fc5c7d2… drh 65 @ subscriberCode BLOB DEFAULT (randomblob(32)) UNIQUE, -- UUID for subscriber
fc5c7d2… drh 66 @ semail TEXT UNIQUE COLLATE nocase,-- email address
fc5c7d2… drh 67 @ suname TEXT, -- corresponding USER entry
fc5c7d2… drh 68 @ sverified BOOLEAN DEFAULT true, -- email address verified
e7a5b98… stephan 69 @ sdonotcall BOOLEAN, -- true for Do Not Call
fc5c7d2… drh 70 @ sdigest BOOLEAN, -- true for daily digests only
fc5c7d2… drh 71 @ ssub TEXT, -- baseline subscriptions
fc5c7d2… drh 72 @ sctime INTDATE, -- When this entry was created. unixtime
fc5c7d2… drh 73 @ mtime INTDATE, -- Last change. unixtime
d7e10ce… drh 74 @ smip TEXT, -- IP address of last change
d7e10ce… drh 75 @ lastContact INT -- Last contact. days since 1970
fc5c7d2… drh 76 @ );
fc5c7d2… drh 77 @ CREATE INDEX repository.subscriberUname
fc5c7d2… drh 78 @ ON subscriber(suname) WHERE suname IS NOT NULL;
e7a5b98… stephan 79 @
fc5c7d2… drh 80 @ DROP TABLE IF EXISTS repository.pending_alert;
fc5c7d2… drh 81 @ -- Email notifications that need to be sent.
fc5c7d2… drh 82 @ --
fc5c7d2… drh 83 @ -- The first character of the eventid determines the event type.
fc5c7d2… drh 84 @ -- Remaining characters determine the specific event. For example,
fc5c7d2… drh 85 @ -- 'c4413' means check-in with rid=4413.
fc5c7d2… drh 86 @ --
fc5c7d2… drh 87 @ CREATE TABLE repository.pending_alert(
fc5c7d2… drh 88 @ eventid TEXT PRIMARY KEY, -- Object that changed
fc5c7d2… drh 89 @ sentSep BOOLEAN DEFAULT false, -- individual alert sent
fc5c7d2… drh 90 @ sentDigest BOOLEAN DEFAULT false, -- digest alert sent
fc5c7d2… drh 91 @ sentMod BOOLEAN DEFAULT false -- pending moderation alert sent
fc5c7d2… drh 92 @ ) WITHOUT ROWID;
e0576ea… stephan 93 @
d7e10ce… drh 94 @ -- Obsolete table. No longer used.
fc5c7d2… drh 95 @ DROP TABLE IF EXISTS repository.alert_bounce;
fc5c7d2… drh 96 ;
fc5c7d2… drh 97
fc5c7d2… drh 98 /*
fc5c7d2… drh 99 ** Return true if the email notification tables exist.
fc5c7d2… drh 100 */
fc5c7d2… drh 101 int alert_tables_exist(void){
fc5c7d2… drh 102 return db_table_exists("repository", "subscriber");
d7e10ce… drh 103 }
d7e10ce… drh 104
d7e10ce… drh 105 /*
d7e10ce… drh 106 ** Record the fact that user zUser has made contact with the repository.
d7e10ce… drh 107 ** This resets the subscription timeout on that user.
d7e10ce… drh 108 */
d7e10ce… drh 109 void alert_user_contact(const char *zUser){
d7e10ce… drh 110 if( db_table_has_column("repository","subscriber","lastContact") ){
db16262… drh 111 db_unprotect(PROTECT_READONLY);
d7e10ce… drh 112 db_multi_exec(
d7e10ce… drh 113 "UPDATE subscriber SET lastContact=now()/86400 WHERE suname=%Q",
d7e10ce… drh 114 zUser
d7e10ce… drh 115 );
db16262… drh 116 db_protect_pop();
d7e10ce… drh 117 }
fc5c7d2… drh 118 }
fc5c7d2… drh 119
fc5c7d2… drh 120 /*
fc5c7d2… drh 121 ** Make sure the table needed for email notification exist in the repository.
fc5c7d2… drh 122 **
fc5c7d2… drh 123 ** If the bOnlyIfEnabled option is true, then tables are only created
fc5c7d2… drh 124 ** if the email-send-method is something other than "off".
fc5c7d2… drh 125 */
fc5c7d2… drh 126 void alert_schema(int bOnlyIfEnabled){
fc5c7d2… drh 127 if( !alert_tables_exist() ){
fc5c7d2… drh 128 if( bOnlyIfEnabled
eb804dc… drh 129 && fossil_strcmp(db_get("email-send-method",0),"off")==0
fc5c7d2… drh 130 ){
fc5c7d2… drh 131 return; /* Don't create table for disabled email */
fc5c7d2… drh 132 }
0ea56bb… drh 133 db_exec_sql(zAlertInit);
d7e10ce… drh 134 return;
0ea56bb… drh 135 }
d7e10ce… drh 136 if( db_table_has_column("repository","subscriber","lastContact") ){
d7e10ce… drh 137 return;
d7e10ce… drh 138 }
f33976f… drh 139 db_unprotect(PROTECT_READONLY);
d7e10ce… drh 140 db_multi_exec(
1e5dc32… mistachkin 141 "DROP TABLE IF EXISTS repository.alert_bounce;\n"
d7e10ce… drh 142 "ALTER TABLE repository.subscriber ADD COLUMN lastContact INT;\n"
d7e10ce… drh 143 "UPDATE subscriber SET lastContact=mtime/86400;"
d7e10ce… drh 144 );
f33976f… drh 145 db_protect_pop();
d7e10ce… drh 146 if( db_table_has_column("repository","pending_alert","sentMod") ){
d7e10ce… drh 147 return;
d7e10ce… drh 148 }
d7e10ce… drh 149 db_multi_exec(
d7e10ce… drh 150 "ALTER TABLE repository.pending_alert"
d7e10ce… drh 151 " ADD COLUMN sentMod BOOLEAN DEFAULT false;"
d7e10ce… drh 152 );
974cf36… drh 153 }
974cf36… drh 154
974cf36… drh 155 /*
c3ed243… drh 156 ** Process deferred alert events. Return the number of errors.
974cf36… drh 157 */
974cf36… drh 158 static int alert_process_deferred_triggers(void){
974cf36… drh 159 if( db_table_exists("temp","deferred_chat_events")
974cf36… drh 160 && db_table_exists("repository","chat")
974cf36… drh 161 ){
974cf36… drh 162 const char *zChatUser = db_get("chat-timeline-user", 0);
974cf36… drh 163 if( zChatUser && zChatUser[0] ){
614b3f7… drh 164 chat_create_tables(); /* Make sure TEMP TRIGGERs for FTS exist */
974cf36… drh 165 db_multi_exec(
e1ad499… stephan 166 "INSERT INTO chat(mtime,lmtime,xfrom,xmsg)"
e1ad499… stephan 167 " SELECT julianday(), "
e1ad499… stephan 168 " strftime('%%Y-%%m-%%dT%%H:%%M:%%S','now','localtime'),"
e1ad499… stephan 169 " %Q,"
974cf36… drh 170 " chat_msg_from_event(type, objid, user, comment)\n"
974cf36… drh 171 " FROM deferred_chat_events;\n",
974cf36… drh 172 zChatUser
974cf36… drh 173 );
974cf36… drh 174 }
974cf36… drh 175 }
974cf36… drh 176 return 0;
e9d7cf3… drh 177 }
e9d7cf3… drh 178
e9d7cf3… drh 179 /*
fc5c7d2… drh 180 ** Enable triggers that automatically populate the pending_alert
e9d7cf3… drh 181 ** table. (Later:) Also add triggers that automatically relay timeline
e9d7cf3… drh 182 ** events to chat, if chat is configured for that.
fc5c7d2… drh 183 */
169ba8d… drh 184 void alert_create_trigger(void){
e9d7cf3… drh 185 if( db_table_exists("repository","pending_alert") ){
e9d7cf3… drh 186 db_multi_exec(
e9d7cf3… drh 187 "DROP TRIGGER IF EXISTS repository.alert_trigger1;\n" /* Purge legacy */
e9d7cf3… drh 188 "CREATE TRIGGER temp.alert_trigger1\n"
e9d7cf3… drh 189 "AFTER INSERT ON repository.event BEGIN\n"
e9d7cf3… drh 190 " INSERT INTO pending_alert(eventid)\n"
e9d7cf3… drh 191 " SELECT printf('%%.1c%%d',new.type,new.objid) WHERE true\n"
e9d7cf3… drh 192 " ON CONFLICT(eventId) DO NOTHING;\n"
e9d7cf3… drh 193 "END;"
e9d7cf3… drh 194 );
e9d7cf3… drh 195 }
974cf36… drh 196 if( db_table_exists("repository","chat")
3f6aa94… stephan 197 && db_get("chat-timeline-user", "")[0]!=0
974cf36… drh 198 ){
974cf36… drh 199 /* Record events that will be relayed to chat, but do not relay
974cf36… drh 200 ** them immediately, as the chat_msg_from_event() function requires
974cf36… drh 201 ** that TAGXREF be up-to-date, and that has not happened yet when
c3ed243… drh 202 ** the insert into the EVENT table occurs. Make arrangements to
c3ed243… drh 203 ** invoke alert_process_deferred_triggers() when the transaction
c3ed243… drh 204 ** commits. The TAGXREF table will be ready by then. */
974cf36… drh 205 db_multi_exec(
974cf36… drh 206 "CREATE TABLE temp.deferred_chat_events(\n"
974cf36… drh 207 " type TEXT,\n"
974cf36… drh 208 " objid INT,\n"
974cf36… drh 209 " user TEXT,\n"
974cf36… drh 210 " comment TEXT\n"
974cf36… drh 211 ");\n"
974cf36… drh 212 "CREATE TRIGGER temp.chat_trigger1\n"
974cf36… drh 213 "AFTER INSERT ON repository.event BEGIN\n"
974cf36… drh 214 " INSERT INTO deferred_chat_events"
974cf36… drh 215 " VALUES(new.type,new.objid,new.user,new.comment);\n"
974cf36… drh 216 "END;\n"
974cf36… drh 217 );
974cf36… drh 218 db_commit_hook(alert_process_deferred_triggers, 1);
e9d7cf3… drh 219 }
fc5c7d2… drh 220 }
fc5c7d2… drh 221
fc5c7d2… drh 222 /*
e9d7cf3… drh 223 ** Disable triggers the event_pending and chat triggers.
fc5c7d2… drh 224 **
fc5c7d2… drh 225 ** This must be called before rebuilding the EVENT table, for example
fc5c7d2… drh 226 ** via the "fossil rebuild" command.
fc5c7d2… drh 227 */
169ba8d… drh 228 void alert_drop_trigger(void){
fc5c7d2… drh 229 db_multi_exec(
169ba8d… drh 230 "DROP TRIGGER IF EXISTS temp.alert_trigger1;\n"
169ba8d… drh 231 "DROP TRIGGER IF EXISTS repository.alert_trigger1;\n" /* Purge legacy */
e9d7cf3… drh 232 "DROP TRIGGER IF EXISTS temp.chat_trigger1;\n"
fc5c7d2… drh 233 );
fc5c7d2… drh 234 }
fc5c7d2… drh 235
fc5c7d2… drh 236 /*
fc5c7d2… drh 237 ** Return true if email alerts are active.
fc5c7d2… drh 238 */
fc5c7d2… drh 239 int alert_enabled(void){
fc5c7d2… drh 240 if( !alert_tables_exist() ) return 0;
eb804dc… drh 241 if( fossil_strcmp(db_get("email-send-method",0),"off")==0 ) return 0;
fc5c7d2… drh 242 return 1;
974cf36… drh 243 }
974cf36… drh 244
974cf36… drh 245 /*
3f6aa94… stephan 246 ** If alerts are enabled, removes the pending_alert entry which
3f6aa94… stephan 247 ** matches (eventType || rid). Note that pending_alert entries are
3f6aa94… stephan 248 ** added via the manifest crosslinking process, so this has no effect
3f6aa94… stephan 249 ** if called before crosslinking is performed. Because alerts are sent
3f6aa94… stephan 250 ** asynchronously, unqueuing needs to be performed as part of the
3f6aa94… stephan 251 ** transaction in which crosslinking is performed in order to avoid a
3f6aa94… stephan 252 ** race condition.
3f6aa94… stephan 253 */
3f6aa94… stephan 254 void alert_unqueue(char eventType, int rid){
3f6aa94… stephan 255 if( alert_enabled() ){
3f6aa94… stephan 256 db_multi_exec("DELETE FROM pending_alert WHERE eventid='%c%d'",
3f6aa94… stephan 257 eventType, rid);
3f6aa94… stephan 258 }
3f6aa94… stephan 259 }
3f6aa94… stephan 260
3f6aa94… stephan 261 /*
fc5c7d2… drh 262 ** If the subscriber table does not exist, then paint an error message
fc5c7d2… drh 263 ** web page and return true.
fc5c7d2… drh 264 **
fc5c7d2… drh 265 ** If the subscriber table does exist, return 0 without doing anything.
fc5c7d2… drh 266 */
fc5c7d2… drh 267 static int alert_webpages_disabled(void){
fc5c7d2… drh 268 if( alert_tables_exist() ) return 0;
112c713… drh 269 style_set_current_feature("alerts");
fc5c7d2… drh 270 style_header("Email Alerts Are Disabled");
fc5c7d2… drh 271 @ <p>Email alerts are disabled on this server</p>
112c713… drh 272 style_finish_page();
fc5c7d2… drh 273 return 1;
fc5c7d2… drh 274 }
fc5c7d2… drh 275
fc5c7d2… drh 276 /*
fc5c7d2… drh 277 ** Insert a "Subscriber List" submenu link if the current user
fc5c7d2… drh 278 ** is an administrator.
fc5c7d2… drh 279 */
fc5c7d2… drh 280 void alert_submenu_common(void){
fc5c7d2… drh 281 if( g.perm.Admin ){
fc5c7d2… drh 282 if( fossil_strcmp(g.zPath,"subscribers") ){
54a6f09… drh 283 style_submenu_element("Subscribers","%R/subscribers");
fc5c7d2… drh 284 }
fc5c7d2… drh 285 if( fossil_strcmp(g.zPath,"subscribe") ){
fc5c7d2… drh 286 style_submenu_element("Add New Subscriber","%R/subscribe");
54a6f09… drh 287 }
4859a91… drh 288 if( fossil_strcmp(g.zPath,"setup_notification") ){
4859a91… drh 289 style_submenu_element("Notification Setup","%R/setup_notification");
4859a91… drh 290 }
fc5c7d2… drh 291 }
fc5c7d2… drh 292 }
fc5c7d2… drh 293
fc5c7d2… drh 294
fc5c7d2… drh 295 /*
fc5c7d2… drh 296 ** WEBPAGE: setup_notification
fc5c7d2… drh 297 **
fc5c7d2… drh 298 ** Administrative page for configuring and controlling email notification.
fc5c7d2… drh 299 ** Normally accessible via the /Admin/Notification menu.
fc5c7d2… drh 300 */
fc5c7d2… drh 301 void setup_notification(void){
fc5c7d2… drh 302 static const char *const azSendMethods[] = {
fc5c7d2… drh 303 "off", "Disabled",
cad72b7… drh 304 "relay", "SMTP relay",
fc5c7d2… drh 305 "db", "Store in a database",
fc5c7d2… drh 306 "dir", "Store in a directory",
cad72b7… drh 307 "pipe", "Pipe to a command",
fc5c7d2… drh 308 };
fc5c7d2… drh 309 login_check_credentials();
fc5c7d2… drh 310 if( !g.perm.Setup ){
fc5c7d2… drh 311 login_needed(0);
fc5c7d2… drh 312 return;
fc5c7d2… drh 313 }
e1962ef… drh 314 db_begin_transaction();
fc5c7d2… drh 315
fc5c7d2… drh 316 alert_submenu_common();
fc5c7d2… drh 317 style_submenu_element("Send Announcement","%R/announce");
112c713… drh 318 style_set_current_feature("alerts");
fc5c7d2… drh 319 style_header("Email Notification Setup");
cad72b7… drh 320 @ <form action="%R/setup_notification" method="post"><div>
cad72b7… drh 321 @ <h1>Status &ensp; <input type="submit" name="submit" value="Refresh"></h1>
cad72b7… drh 322 @ </form>
fc5c7d2… drh 323 @ <table class="label-value">
fc5c7d2… drh 324 if( alert_enabled() ){
fc5c7d2… drh 325 stats_for_email();
fc5c7d2… drh 326 }else{
fc5c7d2… drh 327 @ <th>Disabled</th>
fc5c7d2… drh 328 }
fc5c7d2… drh 329 @ </table>
fc5c7d2… drh 330 @ <hr>
fc5c7d2… drh 331 @ <form action="%R/setup_notification" method="post"><div>
cad72b7… drh 332 @ <h1> Configuration </h1>
cad72b7… drh 333 @ <p><input type="submit" name="submit" value="Apply Changes"></p>
cad72b7… drh 334 @ <hr>
fc5c7d2… drh 335 login_insert_csrf_secret();
fc5c7d2… drh 336
fc5c7d2… drh 337 entry_attribute("Canonical Server URL", 40, "email-url",
fc5c7d2… drh 338 "eurl", "", 0);
fc5c7d2… drh 339 @ <p><b>Required.</b>
fc85382… stephan 340 @ This URL is used as the basename for hyperlinks included in
fc5c7d2… drh 341 @ email alert text. Omit the trailing "/".
fc5c7d2… drh 342 @ Suggested value: "%h(g.zBaseURL)"
fc5c7d2… drh 343 @ (Property: "email-url")</p>
60d40d5… drh 344 @ <hr>
60d40d5… drh 345
60d40d5… drh 346 entry_attribute("Administrator email address", 40, "email-admin",
60d40d5… drh 347 "eadmin", "", 0);
60d40d5… drh 348 @ <p>This is the email for the human administrator for the system.
60d40d5… drh 349 @ Abuse and trouble reports and password reset requests are send here.
60d40d5… drh 350 @ (Property: "email-admin")</p>
fc5c7d2… drh 351 @ <hr>
fc5c7d2… drh 352
fc5c7d2… drh 353 entry_attribute("\"Return-Path\" email address", 20, "email-self",
fc5c7d2… drh 354 "eself", "", 0);
fc5c7d2… drh 355 @ <p><b>Required.</b>
fc5c7d2… drh 356 @ This is the email to which email notification bounces should be sent.
fc5c7d2… drh 357 @ In cases where the email notification does not align with a specific
fc5c7d2… drh 358 @ Fossil login account (for example, digest messages), this is also
fc5c7d2… drh 359 @ the "From:" address of the email notification.
fc5c7d2… drh 360 @ The system administrator should arrange for emails sent to this address
fc5c7d2… drh 361 @ to be handed off to the "fossil email incoming" command so that Fossil
fc5c7d2… drh 362 @ can handle bounces. (Property: "email-self")</p>
fc5c7d2… drh 363 @ <hr>
fc5c7d2… drh 364
e50c362… drh 365 entry_attribute("List-ID", 40, "email-listid",
e50c362… drh 366 "elistid", "", 0);
e50c362… drh 367 @ <p>
e50c362… drh 368 @ If this is not an empty string, then it becomes the argument to
0151018… stephan 369 @ a "List-ID:" header on all out-bound notification emails. A list ID
0151018… stephan 370 @ is required for the generation of unsubscribe links in notifications.
e50c362… drh 371 @ (Property: "email-listid")</p>
e50c362… drh 372 @ <hr>
e50c362… drh 373
fc5c7d2… drh 374 entry_attribute("Repository Nickname", 16, "email-subname",
fc5c7d2… drh 375 "enn", "", 0);
fc5c7d2… drh 376 @ <p><b>Required.</b>
fc5c7d2… drh 377 @ This is short name used to identifies the repository in the
fc5c7d2… drh 378 @ Subject: line of email alerts. Traditionally this name is
fc5c7d2… drh 379 @ included in square brackets. Examples: "[fossil-src]", "[sqlite-src]".
fc5c7d2… drh 380 @ (Property: "email-subname")</p>
fc5c7d2… drh 381 @ <hr>
fc5c7d2… drh 382
7b8be20… drh 383 entry_attribute("Subscription Renewal Interval In Days", 8,
7b8be20… drh 384 "email-renew-interval", "eri", "", 0);
7b8be20… drh 385 @ <p>
99a319b… wyoung 386 @ If this value is an integer N greater than or equal to 14, then email
34d45c5… drh 387 @ notification subscriptions will be suspended N days after the last known
7b8be20… drh 388 @ interaction with the user. This prevents sending notifications
34d45c5… drh 389 @ to abandoned accounts. If a subscription comes within 7 days of expiring,
7b8be20… drh 390 @ a separate email goes out with the daily digest that prompts the
7b8be20… drh 391 @ subscriber to click on a link to the "/renew" webpage in order to
34d45c5… drh 392 @ extend their subscription. Subscriptions never expire if this setting
34d45c5… drh 393 @ is less than 14 or is an empty string.
7b8be20… drh 394 @ (Property: "email-renew-interval")</p>
7b8be20… drh 395 @ <hr>
7b8be20… drh 396
fc5c7d2… drh 397 multiple_choice_attribute("Email Send Method", "email-send-method", "esm",
fc5c7d2… drh 398 "off", count(azSendMethods)/2, azSendMethods);
fc5c7d2… drh 399 @ <p>How to send email. Requires auxiliary information from the fields
fc5c7d2… drh 400 @ that follow. Hint: Use the <a href="%R/announce">/announce</a> page
fc5c7d2… drh 401 @ to send test message to debug this setting.
fc5c7d2… drh 402 @ (Property: "email-send-method")</p>
fc5c7d2… drh 403 alert_schema(1);
cad72b7… drh 404 entry_attribute("SMTP Relay Host", 60, "email-send-relayhost",
6c7066a… drh 405 "esrh", "localhost", 0);
cad72b7… drh 406 @ <p>When the send method is "SMTP relay", each email message is
cad72b7… drh 407 @ transmitted via the SMTP protocol (rfc5321) to a "Mail Submission
cad72b7… drh 408 @ Agent" or "MSA" (rfc4409) at the hostname shown here. Optionally
cad72b7… drh 409 @ append a colon and TCP port number (ex: smtp.example.com:587).
cad72b7… drh 410 @ The default TCP port number is 25.
8266b5b… drh 411 @ Usage Hint: If Fossil is running inside of a chroot jail, then it might
8266b5b… drh 412 @ not be able to resolve hostnames. Work around this by using a raw IP
8266b5b… drh 413 @ address or create a "/etc/hosts" file inside the chroot jail.
cad72b7… drh 414 @ (Property: "email-send-relayhost")</p>
8266b5b… drh 415 @
cad72b7… drh 416 entry_attribute("Store Emails In This Database", 60, "email-send-db",
cad72b7… drh 417 "esdb", "", 0);
cad72b7… drh 418 @ <p>When the send method is "store in a database", each email message is
cad72b7… drh 419 @ stored in an SQLite database file with the name given here.
cad72b7… drh 420 @ (Property: "email-send-db")</p>
9993c43… danshearer 421 entry_attribute("Pipe Email Text Into This Command", 60, "email-send-command",
9993c43… danshearer 422 "ecmd", "sendmail -ti", 0);
9993c43… danshearer 423 @ <p>When the send method is "pipe to a command", this is the command
9993c43… danshearer 424 @ that is run. Email messages are piped into the standard input of this
9993c43… danshearer 425 @ command. The command is expected to extract the sender address,
9993c43… danshearer 426 @ recipient addresses, and subject from the header of the piped email
9993c43… danshearer 427 @ text. (Property: "email-send-command")</p>
fc5c7d2… drh 428 entry_attribute("Store Emails In This Directory", 60, "email-send-dir",
fc5c7d2… drh 429 "esdir", "", 0);
fc5c7d2… drh 430 @ <p>When the send method is "store in a directory", each email message is
fc5c7d2… drh 431 @ stored as a separate file in the directory shown here.
fc5c7d2… drh 432 @ (Property: "email-send-dir")</p>
fc5c7d2… drh 433
60d40d5… drh 434 @ <hr>
60d40d5… drh 435
f5482a0… wyoung 436 @ <p><input type="submit" name="submit" value="Apply Changes"></p>
60d40d5… drh 437 @ </div></form>
60d40d5… drh 438 db_end_transaction(0);
112c713… drh 439 style_finish_page();
fc5c7d2… drh 440 }
fc5c7d2… drh 441
fc5c7d2… drh 442 #if 0
fc5c7d2… drh 443 /*
fc5c7d2… drh 444 ** Encode pMsg as MIME base64 and append it to pOut
fc5c7d2… drh 445 */
fc5c7d2… drh 446 static void append_base64(Blob *pOut, Blob *pMsg){
fc5c7d2… drh 447 int n, i, k;
fc5c7d2… drh 448 char zBuf[100];
fc5c7d2… drh 449 n = blob_size(pMsg);
fc5c7d2… drh 450 for(i=0; i<n; i+=54){
fc5c7d2… drh 451 k = translateBase64(blob_buffer(pMsg)+i, i+54<n ? 54 : n-i, zBuf);
fc5c7d2… drh 452 blob_append(pOut, zBuf, k);
fc5c7d2… drh 453 blob_append(pOut, "\r\n", 2);
fc5c7d2… drh 454 }
fc5c7d2… drh 455 }
fc5c7d2… drh 456 #endif
fc5c7d2… drh 457
fc5c7d2… drh 458 /*
fc5c7d2… drh 459 ** Encode pMsg using the quoted-printable email encoding and
fc5c7d2… drh 460 ** append it onto pOut
fc5c7d2… drh 461 */
fc5c7d2… drh 462 static void append_quoted(Blob *pOut, Blob *pMsg){
fc5c7d2… drh 463 char *zIn = blob_str(pMsg);
fc5c7d2… drh 464 char c;
fc5c7d2… drh 465 int iCol = 0;
fc5c7d2… drh 466 while( (c = *(zIn++))!=0 ){
fc5c7d2… drh 467 if( (c>='!' && c<='~' && c!='=' && c!=':')
fc5c7d2… drh 468 || (c==' ' && zIn[0]!='\r' && zIn[0]!='\n')
fc5c7d2… drh 469 ){
fc5c7d2… drh 470 blob_append_char(pOut, c);
fc5c7d2… drh 471 iCol++;
fc5c7d2… drh 472 if( iCol>=70 ){
fc5c7d2… drh 473 blob_append(pOut, "=\r\n", 3);
fc5c7d2… drh 474 iCol = 0;
fc5c7d2… drh 475 }
fc5c7d2… drh 476 }else if( c=='\r' && zIn[0]=='\n' ){
fc5c7d2… drh 477 zIn++;
fc5c7d2… drh 478 blob_append(pOut, "\r\n", 2);
fc5c7d2… drh 479 iCol = 0;
fc5c7d2… drh 480 }else if( c=='\n' ){
fc5c7d2… drh 481 blob_append(pOut, "\r\n", 2);
fc5c7d2… drh 482 iCol = 0;
fc5c7d2… drh 483 }else{
fc5c7d2… drh 484 char x[3];
fc5c7d2… drh 485 x[0] = '=';
fc5c7d2… drh 486 x[1] = "0123456789ABCDEF"[(c>>4)&0xf];
fc5c7d2… drh 487 x[2] = "0123456789ABCDEF"[c&0xf];
fc5c7d2… drh 488 blob_append(pOut, x, 3);
fc5c7d2… drh 489 iCol += 3;
fc5c7d2… drh 490 }
fc5c7d2… drh 491 }
fc5c7d2… drh 492 }
fc5c7d2… drh 493
fc5c7d2… drh 494 #if INTERFACE
fc5c7d2… drh 495 /*
fc5c7d2… drh 496 ** An instance of the following object is used to send emails.
fc5c7d2… drh 497 */
fc5c7d2… drh 498 struct AlertSender {
fc5c7d2… drh 499 sqlite3 *db; /* Database emails are sent to */
fc5c7d2… drh 500 sqlite3_stmt *pStmt; /* Stmt to insert into the database */
fc5c7d2… drh 501 const char *zDest; /* How to send email. */
fc5c7d2… drh 502 const char *zDb; /* Name of database file */
fc5c7d2… drh 503 const char *zDir; /* Directory in which to store as email files */
fc5c7d2… drh 504 const char *zCmd; /* Command to run for each email */
fc5c7d2… drh 505 const char *zFrom; /* Emails come from here */
e50c362… drh 506 const char *zListId; /* Argument to List-ID header */
fc5c7d2… drh 507 SmtpSession *pSmtp; /* SMTP relay connection */
fc5c7d2… drh 508 Blob out; /* For zDest=="blob" */
fc5c7d2… drh 509 char *zErr; /* Error message */
fc5c7d2… drh 510 u32 mFlags; /* Flags */
fc5c7d2… drh 511 int bImmediateFail; /* On any error, call fossil_fatal() */
fc5c7d2… drh 512 };
fc5c7d2… drh 513
fc5c7d2… drh 514 /* Allowed values for mFlags to alert_sender_new().
fc5c7d2… drh 515 */
fc5c7d2… drh 516 #define ALERT_IMMEDIATE_FAIL 0x0001 /* Call fossil_fatal() on any error */
fc5c7d2… drh 517 #define ALERT_TRACE 0x0002 /* Log sending process on console */
fc5c7d2… drh 518
fc5c7d2… drh 519 #endif /* INTERFACE */
fc5c7d2… drh 520
fc5c7d2… drh 521 /*
fc5c7d2… drh 522 ** Shutdown an emailer. Clear all information other than the error message.
fc5c7d2… drh 523 */
fc5c7d2… drh 524 static void emailerShutdown(AlertSender *p){
fc5c7d2… drh 525 sqlite3_finalize(p->pStmt);
fc5c7d2… drh 526 p->pStmt = 0;
fc5c7d2… drh 527 sqlite3_close(p->db);
fc5c7d2… drh 528 p->db = 0;
fc5c7d2… drh 529 p->zDb = 0;
fc5c7d2… drh 530 p->zDir = 0;
fc5c7d2… drh 531 p->zCmd = 0;
e50c362… drh 532 p->zListId = 0;
fc5c7d2… drh 533 if( p->pSmtp ){
fc5c7d2… drh 534 smtp_client_quit(p->pSmtp);
fc5c7d2… drh 535 smtp_session_free(p->pSmtp);
fc5c7d2… drh 536 p->pSmtp = 0;
fc5c7d2… drh 537 }
fc5c7d2… drh 538 blob_reset(&p->out);
fc5c7d2… drh 539 }
fc5c7d2… drh 540
fc5c7d2… drh 541 /*
fc5c7d2… drh 542 ** Put the AlertSender into an error state.
fc5c7d2… drh 543 */
fc5c7d2… drh 544 static void emailerError(AlertSender *p, const char *zFormat, ...){
fc5c7d2… drh 545 va_list ap;
fc5c7d2… drh 546 fossil_free(p->zErr);
fc5c7d2… drh 547 va_start(ap, zFormat);
fc5c7d2… drh 548 p->zErr = vmprintf(zFormat, ap);
fc5c7d2… drh 549 va_end(ap);
fc5c7d2… drh 550 emailerShutdown(p);
fc5c7d2… drh 551 if( p->mFlags & ALERT_IMMEDIATE_FAIL ){
fc5c7d2… drh 552 fossil_fatal("%s", p->zErr);
fc5c7d2… drh 553 }
fc5c7d2… drh 554 }
fc5c7d2… drh 555
fc5c7d2… drh 556 /*
fc5c7d2… drh 557 ** Free an email sender object
fc5c7d2… drh 558 */
fc5c7d2… drh 559 void alert_sender_free(AlertSender *p){
fc5c7d2… drh 560 if( p ){
fc5c7d2… drh 561 emailerShutdown(p);
fc5c7d2… drh 562 fossil_free(p->zErr);
fc5c7d2… drh 563 fossil_free(p);
fc5c7d2… drh 564 }
fc5c7d2… drh 565 }
fc5c7d2… drh 566
fc5c7d2… drh 567 /*
fc5c7d2… drh 568 ** Get an email setting value. Report an error if not configured.
fc5c7d2… drh 569 ** Return 0 on success and one if there is an error.
fc5c7d2… drh 570 */
fc5c7d2… drh 571 static int emailerGetSetting(
fc5c7d2… drh 572 AlertSender *p, /* Where to report the error */
fc5c7d2… drh 573 const char **pzVal, /* Write the setting value here */
fc5c7d2… drh 574 const char *zName /* Name of the setting */
fc5c7d2… drh 575 ){
fc5c7d2… drh 576 const char *z = db_get(zName, 0);
fc5c7d2… drh 577 int rc = 0;
fc5c7d2… drh 578 if( z==0 || z[0]==0 ){
fc5c7d2… drh 579 emailerError(p, "missing \"%s\" setting", zName);
fc5c7d2… drh 580 rc = 1;
fc5c7d2… drh 581 }else{
fc5c7d2… drh 582 *pzVal = z;
fc5c7d2… drh 583 }
fc5c7d2… drh 584 return rc;
fc5c7d2… drh 585 }
fc5c7d2… drh 586
fc5c7d2… drh 587 /*
fc5c7d2… drh 588 ** Create a new AlertSender object.
fc5c7d2… drh 589 **
fc5c7d2… drh 590 ** The method used for sending email is determined by various email-*
fc5c7d2… drh 591 ** settings, and especially email-send-method. The repository
fc5c7d2… drh 592 ** email-send-method can be overridden by the zAltDest argument to
fc5c7d2… drh 593 ** cause a different sending mechanism to be used. Pass "stdout" to
fc5c7d2… drh 594 ** zAltDest to cause all emails to be printed to the console for
fc5c7d2… drh 595 ** debugging purposes.
fc5c7d2… drh 596 **
fc5c7d2… drh 597 ** The AlertSender object returned must be freed using alert_sender_free().
fc5c7d2… drh 598 */
fc5c7d2… drh 599 AlertSender *alert_sender_new(const char *zAltDest, u32 mFlags){
fc5c7d2… drh 600 AlertSender *p;
fc5c7d2… drh 601
fc5c7d2… drh 602 p = fossil_malloc(sizeof(*p));
fc5c7d2… drh 603 memset(p, 0, sizeof(*p));
fc5c7d2… drh 604 blob_init(&p->out, 0, 0);
fc5c7d2… drh 605 p->mFlags = mFlags;
fc5c7d2… drh 606 if( zAltDest ){
fc5c7d2… drh 607 p->zDest = zAltDest;
fc5c7d2… drh 608 }else{
eb804dc… drh 609 p->zDest = db_get("email-send-method",0);
fc5c7d2… drh 610 }
fc5c7d2… drh 611 if( fossil_strcmp(p->zDest,"off")==0 ) return p;
fc5c7d2… drh 612 if( emailerGetSetting(p, &p->zFrom, "email-self") ) return p;
e50c362… drh 613 p->zListId = db_get("email-listid", 0);
fc5c7d2… drh 614 if( fossil_strcmp(p->zDest,"db")==0 ){
fc5c7d2… drh 615 char *zErr;
fc5c7d2… drh 616 int rc;
fc5c7d2… drh 617 if( emailerGetSetting(p, &p->zDb, "email-send-db") ) return p;
fc5c7d2… drh 618 rc = sqlite3_open(p->zDb, &p->db);
fc5c7d2… drh 619 if( rc ){
fc5c7d2… drh 620 emailerError(p, "unable to open output database file \"%s\": %s",
fc5c7d2… drh 621 p->zDb, sqlite3_errmsg(p->db));
fc5c7d2… drh 622 return p;
fc5c7d2… drh 623 }
fc5c7d2… drh 624 rc = sqlite3_exec(p->db, "CREATE TABLE IF NOT EXISTS email(\n"
fc5c7d2… drh 625 " emailid INTEGER PRIMARY KEY,\n"
fc5c7d2… drh 626 " msg TEXT\n);", 0, 0, &zErr);
fc5c7d2… drh 627 if( zErr ){
fc5c7d2… drh 628 emailerError(p, "CREATE TABLE failed with \"%s\"", zErr);
fc5c7d2… drh 629 sqlite3_free(zErr);
fc5c7d2… drh 630 return p;
fc5c7d2… drh 631 }
fc5c7d2… drh 632 rc = sqlite3_prepare_v2(p->db, "INSERT INTO email(msg) VALUES(?1)", -1,
fc5c7d2… drh 633 &p->pStmt, 0);
fc5c7d2… drh 634 if( rc ){
fc5c7d2… drh 635 emailerError(p, "cannot prepare INSERT statement: %s",
fc5c7d2… drh 636 sqlite3_errmsg(p->db));
fc5c7d2… drh 637 return p;
fc5c7d2… drh 638 }
fc5c7d2… drh 639 }else if( fossil_strcmp(p->zDest, "pipe")==0 ){
fc5c7d2… drh 640 emailerGetSetting(p, &p->zCmd, "email-send-command");
fc5c7d2… drh 641 }else if( fossil_strcmp(p->zDest, "dir")==0 ){
fc5c7d2… drh 642 emailerGetSetting(p, &p->zDir, "email-send-dir");
fc5c7d2… drh 643 }else if( fossil_strcmp(p->zDest, "blob")==0 ){
fc5c7d2… drh 644 blob_init(&p->out, 0, 0);
bbfca4c… drh 645 }else if( fossil_strcmp(p->zDest, "relay")==0
bbfca4c… drh 646 || fossil_strcmp(p->zDest, "debug-relay")==0
bbfca4c… drh 647 ){
fc5c7d2… drh 648 const char *zRelay = 0;
fc5c7d2… drh 649 emailerGetSetting(p, &zRelay, "email-send-relayhost");
fc5c7d2… drh 650 if( zRelay ){
fc5c7d2… drh 651 u32 smtpFlags = SMTP_DIRECT;
fc5c7d2… drh 652 if( mFlags & ALERT_TRACE ) smtpFlags |= SMTP_TRACE_STDOUT;
bbfca4c… drh 653 blob_init(&p->out, 0, 0);
e7a5b98… stephan 654 p->pSmtp = smtp_session_new(domain_of_addr(p->zFrom), zRelay,
3fc9b90… drh 655 smtpFlags, 0);
056c83d… drh 656 if( p->pSmtp==0 || p->pSmtp->zErr ){
056c83d… drh 657 emailerError(p, "Could not start SMTP session: %s",
056c83d… drh 658 p->pSmtp ? p->pSmtp->zErr : "reason unknown");
056c83d… drh 659 }else if( p->zDest[0]=='d' ){
bbfca4c… drh 660 smtp_session_config(p->pSmtp, SMTP_TRACE_BLOB, &p->out);
bbfca4c… drh 661 }
fc5c7d2… drh 662 }
fc5c7d2… drh 663 }
fc5c7d2… drh 664 return p;
fc5c7d2… drh 665 }
fc5c7d2… drh 666
fc5c7d2… drh 667 /*
fc5c7d2… drh 668 ** Scan the header of the email message in pMsg looking for the
e2bdc10… danield 669 ** (first) occurrence of zField. Fill pValue with the content of
fc5c7d2… drh 670 ** that field.
fc5c7d2… drh 671 **
fc5c7d2… drh 672 ** This routine initializes pValue. Any prior content of pValue is
fc5c7d2… drh 673 ** discarded (leaked).
fc5c7d2… drh 674 **
fc5c7d2… drh 675 ** Return non-zero on success. Return 0 if no instance of the header
fc5c7d2… drh 676 ** is found.
fc5c7d2… drh 677 */
fc5c7d2… drh 678 int email_header_value(Blob *pMsg, const char *zField, Blob *pValue){
fc5c7d2… drh 679 int nField = (int)strlen(zField);
fc5c7d2… drh 680 Blob line;
fc5c7d2… drh 681 blob_rewind(pMsg);
fc5c7d2… drh 682 blob_init(pValue,0,0);
fc5c7d2… drh 683 while( blob_line(pMsg, &line) ){
fc5c7d2… drh 684 int n, i;
fc5c7d2… drh 685 char *z;
fc5c7d2… drh 686 blob_trim(&line);
fc5c7d2… drh 687 n = blob_size(&line);
fc5c7d2… drh 688 if( n==0 ) return 0;
fc5c7d2… drh 689 if( n<nField+1 ) continue;
fc5c7d2… drh 690 z = blob_buffer(&line);
fc5c7d2… drh 691 if( sqlite3_strnicmp(z, zField, nField)==0 && z[nField]==':' ){
fc5c7d2… drh 692 for(i=nField+1; i<n && fossil_isspace(z[i]); i++){}
fc5c7d2… drh 693 blob_init(pValue, z+i, n-i);
fc5c7d2… drh 694 while( blob_line(pMsg, &line) ){
fc5c7d2… drh 695 blob_trim(&line);
fc5c7d2… drh 696 n = blob_size(&line);
fc5c7d2… drh 697 if( n==0 ) break;
fc5c7d2… drh 698 z = blob_buffer(&line);
fc5c7d2… drh 699 if( !fossil_isspace(z[0]) ) break;
fc5c7d2… drh 700 for(i=1; i<n && fossil_isspace(z[i]); i++){}
fc5c7d2… drh 701 blob_append(pValue, " ", 1);
fc5c7d2… drh 702 blob_append(pValue, z+i, n-i);
fc5c7d2… drh 703 }
fc5c7d2… drh 704 return 1;
fc5c7d2… drh 705 }
fc5c7d2… drh 706 }
fc5c7d2… drh 707 return 0;
fc5c7d2… drh 708 }
fc5c7d2… drh 709
fc5c7d2… drh 710 /*
32a8d11… drh 711 ** Determine whether or not the input string is a valid email address.
32a8d11… drh 712 ** Only look at character up to but not including the first \000 or
32a8d11… drh 713 ** the first cTerm character, whichever comes first.
32a8d11… drh 714 **
e2bdc10… danield 715 ** Return the length of the email address string in bytes if the email
32a8d11… drh 716 ** address is valid. If the email address is misformed, return 0.
32a8d11… drh 717 */
32a8d11… drh 718 int email_address_is_valid(const char *z, char cTerm){
fc5c7d2… drh 719 int i;
fc5c7d2… drh 720 int nAt = 0;
fc5c7d2… drh 721 int nDot = 0;
fc5c7d2… drh 722 char c;
fc5c7d2… drh 723 if( z[0]=='.' ) return 0; /* Local part cannot begin with "." */
fc5c7d2… drh 724 for(i=0; (c = z[i])!=0 && c!=cTerm; i++){
fc5c7d2… drh 725 if( fossil_isalnum(c) ){
fc5c7d2… drh 726 /* Alphanumerics are always ok */
fc5c7d2… drh 727 }else if( c=='@' ){
fc5c7d2… drh 728 if( nAt ) return 0; /* Only a single "@" allowed */
fc5c7d2… drh 729 if( i>64 ) return 0; /* Local part too big */
fc5c7d2… drh 730 nAt = 1;
fc5c7d2… drh 731 nDot = 0;
fc5c7d2… drh 732 if( i==0 ) return 0; /* Disallow empty local part */
fc5c7d2… drh 733 if( z[i-1]=='.' ) return 0; /* Last char of local cannot be "." */
fc5c7d2… drh 734 if( z[i+1]=='.' || z[i+1]=='-' ){
fc5c7d2… drh 735 return 0; /* Domain cannot begin with "." or "-" */
fc5c7d2… drh 736 }
fc5c7d2… drh 737 }else if( c=='-' ){
fc5c7d2… drh 738 if( z[i+1]==cTerm ) return 0; /* Last character cannot be "-" */
fc5c7d2… drh 739 }else if( c=='.' ){
fc5c7d2… drh 740 if( z[i+1]=='.' ) return 0; /* Do not allow ".." */
fc5c7d2… drh 741 if( z[i+1]==cTerm ) return 0; /* Domain may not end with . */
fc5c7d2… drh 742 nDot++;
fc5c7d2… drh 743 }else if( (c=='_' || c=='+') && nAt==0 ){
fc5c7d2… drh 744 /* _ and + are ok in the local part */
fc5c7d2… drh 745 }else{
fc5c7d2… drh 746 return 0; /* Anything else is an error */
fc5c7d2… drh 747 }
fc5c7d2… drh 748 }
fc5c7d2… drh 749 if( c!=cTerm ) return 0; /* Missing terminator */
fc5c7d2… drh 750 if( nAt==0 ) return 0; /* No "@" found anywhere */
fc5c7d2… drh 751 if( nDot==0 ) return 0; /* No "." in the domain */
32a8d11… drh 752 return i;
32a8d11… drh 753 }
fc5c7d2… drh 754
32a8d11… drh 755 /*
32a8d11… drh 756 ** Make a copy of the input string up to but not including the
32a8d11… drh 757 ** first cTerm character.
32a8d11… drh 758 **
f974583… mark 759 ** Verify that the string to be copied really is a valid
f974583… mark 760 ** email address. If it is not, then return NULL.
32a8d11… drh 761 **
32a8d11… drh 762 ** This routine is more restrictive than necessary. It does not
32a8d11… drh 763 ** allow comments, IP address, quoted strings, or certain uncommon
32a8d11… drh 764 ** characters. The only non-alphanumerics allowed in the local
32a8d11… drh 765 ** part are "_", "+", "-" and "+".
32a8d11… drh 766 */
32a8d11… drh 767 char *email_copy_addr(const char *z, char cTerm ){
32a8d11… drh 768 int i = email_address_is_valid(z, cTerm);
32a8d11… drh 769 return i==0 ? 0 : mprintf("%.*s", i, z);
fc5c7d2… drh 770 }
fc5c7d2… drh 771
fc5c7d2… drh 772 /*
f974583… mark 773 ** Scan the input string for a valid email address that may be
f974583… mark 774 ** enclosed in <...>, or delimited by ',' or ':' or '=' or ' '.
fc5c7d2… drh 775 ** If the string contains one or more email addresses, extract the first
fc5c7d2… drh 776 ** one into memory obtained from mprintf() and return a pointer to it.
fc5c7d2… drh 777 ** If no valid email address can be found, return NULL.
fc5c7d2… drh 778 */
fc5c7d2… drh 779 char *alert_find_emailaddr(const char *zIn){
fc5c7d2… drh 780 char *zOut = 0;
f974583… mark 781 do{
f974583… mark 782 zOut = email_copy_addr(zIn, zIn[strcspn(zIn, ">,:= ")]);
f974583… mark 783 if( zOut!=0 ) break;
f974583… mark 784 zIn = (const char *)strpbrk(zIn, "<,:= ");
f974583… mark 785 if( zIn==0 ) break;
f974583… mark 786 zIn++;
f974583… mark 787 }while( zIn!=0 );
fc5c7d2… drh 788 return zOut;
fc5c7d2… drh 789 }
fc5c7d2… drh 790
fc5c7d2… drh 791 /*
fc5c7d2… drh 792 ** SQL function: find_emailaddr(X)
fc5c7d2… drh 793 **
fc5c7d2… drh 794 ** Return the first valid email address of the form <...> in input string
fc5c7d2… drh 795 ** X. Or return NULL if not found.
fc5c7d2… drh 796 */
fc5c7d2… drh 797 void alert_find_emailaddr_func(
fc5c7d2… drh 798 sqlite3_context *context,
fc5c7d2… drh 799 int argc,
fc5c7d2… drh 800 sqlite3_value **argv
fc5c7d2… drh 801 ){
fc5c7d2… drh 802 const char *zIn = (const char*)sqlite3_value_text(argv[0]);
fc5c7d2… drh 803 char *zOut = alert_find_emailaddr(zIn);
fc5c7d2… drh 804 if( zOut ){
fc5c7d2… drh 805 sqlite3_result_text(context, zOut, -1, fossil_free);
2e71dc2… drh 806 }
2e71dc2… drh 807 }
2e71dc2… drh 808
2e71dc2… drh 809 /*
2e71dc2… drh 810 ** SQL function: display_name(X)
2e71dc2… drh 811 **
2e71dc2… drh 812 ** If X is a string, search for a user name at the beginning of that
2e71dc2… drh 813 ** string. The user name must be followed by an email address. If
2e71dc2… drh 814 ** found, return the user name. If not found, return NULL.
2e71dc2… drh 815 **
2e71dc2… drh 816 ** This routine is used to extract the display name from the USER.INFO
2e71dc2… drh 817 ** field.
2e71dc2… drh 818 */
2e71dc2… drh 819 void alert_display_name_func(
2e71dc2… drh 820 sqlite3_context *context,
2e71dc2… drh 821 int argc,
2e71dc2… drh 822 sqlite3_value **argv
2e71dc2… drh 823 ){
2e71dc2… drh 824 const char *zIn = (const char*)sqlite3_value_text(argv[0]);
2e71dc2… drh 825 int i;
2e71dc2… drh 826 if( zIn==0 ) return;
2e71dc2… drh 827 while( fossil_isspace(zIn[0]) ) zIn++;
2e71dc2… drh 828 for(i=0; zIn[i] && zIn[i]!='<' && zIn[i]!='\n'; i++){}
2e71dc2… drh 829 if( zIn[i]=='<' ){
2e71dc2… drh 830 while( i>0 && fossil_isspace(zIn[i-1]) ){ i--; }
2e71dc2… drh 831 if( i>0 ){
2e71dc2… drh 832 sqlite3_result_text(context, zIn, i, SQLITE_TRANSIENT);
2e71dc2… drh 833 }
fc5c7d2… drh 834 }
fc5c7d2… drh 835 }
fc5c7d2… drh 836
fc5c7d2… drh 837 /*
fc5c7d2… drh 838 ** Return the hostname portion of an email address - the part following
fc5c7d2… drh 839 ** the @
fc5c7d2… drh 840 */
fc5c7d2… drh 841 char *alert_hostname(const char *zAddr){
fc5c7d2… drh 842 char *z = strchr(zAddr, '@');
fc5c7d2… drh 843 if( z ){
fc5c7d2… drh 844 z++;
fc5c7d2… drh 845 }else{
fc5c7d2… drh 846 z = (char*)zAddr;
fc5c7d2… drh 847 }
fc5c7d2… drh 848 return z;
fc5c7d2… drh 849 }
fc5c7d2… drh 850
fc5c7d2… drh 851 /*
fc5c7d2… drh 852 ** Return a pointer to a fake email mailbox name that corresponds
fc5c7d2… drh 853 ** to human-readable name zFromName. The fake mailbox name is based
fc5c7d2… drh 854 ** on a hash. No huge problems arise if there is a hash collisions,
fc5c7d2… drh 855 ** but it is still better if collisions can be avoided.
fc5c7d2… drh 856 **
fc5c7d2… drh 857 ** The returned string is held in a static buffer and is overwritten
fc5c7d2… drh 858 ** by each subsequent call to this routine.
fc5c7d2… drh 859 */
fc5c7d2… drh 860 static char *alert_mailbox_name(const char *zFromName){
fc5c7d2… drh 861 static char zHash[20];
fc5c7d2… drh 862 unsigned int x = 0;
fc5c7d2… drh 863 int n = 0;
fc5c7d2… drh 864 while( zFromName[0] ){
fc5c7d2… drh 865 n++;
fc5c7d2… drh 866 x = x*1103515245 + 12345 + ((unsigned char*)zFromName)[0];
fc5c7d2… drh 867 zFromName++;
fc5c7d2… drh 868 }
fc5c7d2… drh 869 sqlite3_snprintf(sizeof(zHash), zHash,
fc5c7d2… drh 870 "noreply%x%08x", n, x);
fc5c7d2… drh 871 return zHash;
fc5c7d2… drh 872 }
fc5c7d2… drh 873
fc5c7d2… drh 874 /*
fc5c7d2… drh 875 ** COMMAND: test-mailbox-hashname
fc5c7d2… drh 876 **
fc5c7d2… drh 877 ** Usage: %fossil test-mailbox-hashname HUMAN-NAME ...
fc5c7d2… drh 878 **
fc5c7d2… drh 879 ** Return the mailbox hash name corresponding to each human-readable
fc5c7d2… drh 880 ** name on the command line. This is a test interface for the
fc5c7d2… drh 881 ** alert_mailbox_name() function.
fc5c7d2… drh 882 */
fc5c7d2… drh 883 void alert_test_mailbox_hashname(void){
fc5c7d2… drh 884 int i;
fc5c7d2… drh 885 for(i=2; i<g.argc; i++){
fc5c7d2… drh 886 fossil_print("%30s: %s\n", g.argv[i], alert_mailbox_name(g.argv[i]));
fc5c7d2… drh 887 }
fc5c7d2… drh 888 }
fc5c7d2… drh 889
fc5c7d2… drh 890 /*
fc5c7d2… drh 891 ** Extract all To: header values from the email header supplied.
fc5c7d2… drh 892 ** Store them in the array list.
fc5c7d2… drh 893 */
fc5c7d2… drh 894 void email_header_to(Blob *pMsg, int *pnTo, char ***pazTo){
fc5c7d2… drh 895 int nTo = 0;
fc5c7d2… drh 896 char **azTo = 0;
fc5c7d2… drh 897 Blob v;
fc5c7d2… drh 898 char *z, *zAddr;
fc5c7d2… drh 899 int i;
e0576ea… stephan 900
fc5c7d2… drh 901 email_header_value(pMsg, "to", &v);
fc5c7d2… drh 902 z = blob_str(&v);
fc5c7d2… drh 903 for(i=0; z[i]; i++){
fc5c7d2… drh 904 if( z[i]=='<' && (zAddr = email_copy_addr(&z[i+1],'>'))!=0 ){
fc5c7d2… drh 905 azTo = fossil_realloc(azTo, sizeof(azTo[0])*(nTo+1) );
fc5c7d2… drh 906 azTo[nTo++] = zAddr;
fc5c7d2… drh 907 }
fc5c7d2… drh 908 }
fc5c7d2… drh 909 *pnTo = nTo;
fc5c7d2… drh 910 *pazTo = azTo;
fc5c7d2… drh 911 }
fc5c7d2… drh 912
fc5c7d2… drh 913 /*
e0576ea… stephan 914 ** Free a list of To addresses obtained from a prior call to
fc5c7d2… drh 915 ** email_header_to()
fc5c7d2… drh 916 */
fc5c7d2… drh 917 void email_header_to_free(int nTo, char **azTo){
fc5c7d2… drh 918 int i;
fc5c7d2… drh 919 for(i=0; i<nTo; i++) fossil_free(azTo[i]);
fc5c7d2… drh 920 fossil_free(azTo);
fc5c7d2… drh 921 }
fc5c7d2… drh 922
fc5c7d2… drh 923 /*
fc5c7d2… drh 924 ** Send a single email message.
fc5c7d2… drh 925 **
9993c43… danshearer 926 ** The recipient(s) must be specified using "To:" or "Cc:" or "Bcc:" fields
fc5c7d2… drh 927 ** in the header. Likewise, the header must contains a "Subject:" line.
fc5c7d2… drh 928 ** The header might also include fields like "Message-Id:" or
fc5c7d2… drh 929 ** "In-Reply-To:".
fc5c7d2… drh 930 **
fc5c7d2… drh 931 ** This routine will add fields to the header as follows:
fc5c7d2… drh 932 **
fc5c7d2… drh 933 ** From:
fc5c7d2… drh 934 ** Date:
fc5c7d2… drh 935 ** Message-Id:
fc5c7d2… drh 936 ** Content-Type:
fc5c7d2… drh 937 ** Content-Transfer-Encoding:
fc5c7d2… drh 938 ** MIME-Version:
b4dcf8e… drh 939 ** Sender:
e0576ea… stephan 940 **
fc5c7d2… drh 941 ** The caller maintains ownership of the input Blobs. This routine will
fc5c7d2… drh 942 ** read the Blobs and send them onward to the email system, but it will
fc5c7d2… drh 943 ** not free them.
fc5c7d2… drh 944 **
fc5c7d2… drh 945 ** The Message-Id: field is added if there is not already a Message-Id
fc5c7d2… drh 946 ** in the pHdr parameter.
fc5c7d2… drh 947 **
fc5c7d2… drh 948 ** If the zFromName argument is not NULL, then it should be a human-readable
fc5c7d2… drh 949 ** name or handle for the sender. In that case, "From:" becomes a made-up
fc5c7d2… drh 950 ** email address based on a hash of zFromName and the domain of email-self,
b4dcf8e… drh 951 ** and an additional "Sender:" field is inserted with the email-self
b4dcf8e… drh 952 ** address. Downstream software might use the Sender header to set
e0576ea… stephan 953 ** the envelope-from address of the email. If zFromName is a NULL pointer,
b4dcf8e… drh 954 ** then the "From:" is set to the email-self value and Sender is
fc5c7d2… drh 955 ** omitted.
fc5c7d2… drh 956 */
fc5c7d2… drh 957 void alert_send(
fc5c7d2… drh 958 AlertSender *p, /* Emailer context */
fc5c7d2… drh 959 Blob *pHdr, /* Email header (incomplete) */
fc5c7d2… drh 960 Blob *pBody, /* Email body */
fc5c7d2… drh 961 const char *zFromName /* Optional human-readable name of sender */
fc5c7d2… drh 962 ){
fc5c7d2… drh 963 Blob all, *pOut;
fc5c7d2… drh 964 u64 r1, r2;
fc5c7d2… drh 965 if( p->mFlags & ALERT_TRACE ){
fc5c7d2… drh 966 fossil_print("Sending email\n");
fc5c7d2… drh 967 }
fc5c7d2… drh 968 if( fossil_strcmp(p->zDest, "off")==0 ){
fc5c7d2… drh 969 return;
fc5c7d2… drh 970 }
ea4ccfd… drh 971 blob_init(&all, 0, 0);
fc5c7d2… drh 972 if( fossil_strcmp(p->zDest, "blob")==0 ){
fc5c7d2… drh 973 pOut = &p->out;
fc5c7d2… drh 974 if( blob_size(pOut) ){
fc5c7d2… drh 975 blob_appendf(pOut, "%.72c\n", '=');
fc5c7d2… drh 976 }
fc5c7d2… drh 977 }else{
fc5c7d2… drh 978 pOut = &all;
fc5c7d2… drh 979 }
fc5c7d2… drh 980 blob_append(pOut, blob_buffer(pHdr), blob_size(pHdr));
488af36… drh 981 if( p->zFrom==0 || p->zFrom[0]==0 ){
4c73b4a… drh 982 return; /* email-self is not set. Error will be reported separately */
488af36… drh 983 }else if( zFromName ){
fc5c7d2… drh 984 blob_appendf(pOut, "From: %s <%s@%s>\r\n",
fc5c7d2… drh 985 zFromName, alert_mailbox_name(zFromName), alert_hostname(p->zFrom));
b4dcf8e… drh 986 blob_appendf(pOut, "Sender: <%s>\r\n", p->zFrom);
fc5c7d2… drh 987 }else{
fc5c7d2… drh 988 blob_appendf(pOut, "From: <%s>\r\n", p->zFrom);
fc5c7d2… drh 989 }
fc5c7d2… drh 990 blob_appendf(pOut, "Date: %z\r\n", cgi_rfc822_datestamp(time(0)));
fc5c7d2… drh 991 if( strstr(blob_str(pHdr), "\r\nMessage-Id:")==0 ){
fc5c7d2… drh 992 /* Message-id format: "<$(date)x$(random)@$(from-host)>" where $(date) is
fc5c7d2… drh 993 ** the current unix-time in hex, $(random) is a 64-bit random number,
fc5c7d2… drh 994 ** and $(from) is the domain part of the email-self setting. */
fc5c7d2… drh 995 sqlite3_randomness(sizeof(r1), &r1);
fc5c7d2… drh 996 r2 = time(0);
fc5c7d2… drh 997 blob_appendf(pOut, "Message-Id: <%llxx%016llx@%s>\r\n",
fc5c7d2… drh 998 r2, r1, alert_hostname(p->zFrom));
fc5c7d2… drh 999 }
fc5c7d2… drh 1000 blob_add_final_newline(pBody);
fc5c7d2… drh 1001 blob_appendf(pOut, "MIME-Version: 1.0\r\n");
fc5c7d2… drh 1002 blob_appendf(pOut, "Content-Type: text/plain; charset=\"UTF-8\"\r\n");
fc5c7d2… drh 1003 #if 0
fc5c7d2… drh 1004 blob_appendf(pOut, "Content-Transfer-Encoding: base64\r\n\r\n");
fc5c7d2… drh 1005 append_base64(pOut, pBody);
fc5c7d2… drh 1006 #else
fc5c7d2… drh 1007 blob_appendf(pOut, "Content-Transfer-Encoding: quoted-printable\r\n\r\n");
fc5c7d2… drh 1008 append_quoted(pOut, pBody);
fc5c7d2… drh 1009 #endif
fc5c7d2… drh 1010 if( p->pStmt ){
fc5c7d2… drh 1011 int i, rc;
fc5c7d2… drh 1012 sqlite3_bind_text(p->pStmt, 1, blob_str(&all), -1, SQLITE_TRANSIENT);
fc5c7d2… drh 1013 for(i=0; i<100 && sqlite3_step(p->pStmt)==SQLITE_BUSY; i++){
fc5c7d2… drh 1014 sqlite3_sleep(10);
fc5c7d2… drh 1015 }
fc5c7d2… drh 1016 rc = sqlite3_reset(p->pStmt);
fc5c7d2… drh 1017 if( rc!=SQLITE_OK ){
fc5c7d2… drh 1018 emailerError(p, "Failed to insert email message into output queue.\n"
fc5c7d2… drh 1019 "%s", sqlite3_errmsg(p->db));
fc5c7d2… drh 1020 }
fc5c7d2… drh 1021 }else if( p->zCmd ){
fc5c7d2… drh 1022 FILE *out = popen(p->zCmd, "w");
fc5c7d2… drh 1023 if( out ){
fc5c7d2… drh 1024 fwrite(blob_buffer(&all), 1, blob_size(&all), out);
1f4b4fc… wyoung 1025 pclose(out);
fc5c7d2… drh 1026 }else{
fc5c7d2… drh 1027 emailerError(p, "Could not open output pipe \"%s\"", p->zCmd);
fc5c7d2… drh 1028 }
fc5c7d2… drh 1029 }else if( p->zDir ){
fc5c7d2… drh 1030 char *zFile = file_time_tempname(p->zDir, ".email");
fc5c7d2… drh 1031 blob_write_to_file(&all, zFile);
fc5c7d2… drh 1032 fossil_free(zFile);
fc5c7d2… drh 1033 }else if( p->pSmtp ){
fc5c7d2… drh 1034 char **azTo = 0;
fc5c7d2… drh 1035 int nTo = 0;
e6c27d3… drh 1036 SmtpSession *pSmtp = p->pSmtp;
fc5c7d2… drh 1037 email_header_to(pHdr, &nTo, &azTo);
e6c27d3… drh 1038 if( nTo>0 && !pSmtp->bFatal ){
e6c27d3… drh 1039 smtp_send_msg(pSmtp,p->zFrom,nTo,(const char**)azTo,blob_str(&all));
e6c27d3… drh 1040 if( pSmtp->zErr && !pSmtp->bFatal ){
e6c27d3… drh 1041 smtp_send_msg(pSmtp,p->zFrom,nTo,(const char**)azTo,blob_str(&all));
e6c27d3… drh 1042 }
e6c27d3… drh 1043 if( pSmtp->zErr ){
e6c27d3… drh 1044 fossil_errorlog("SMTP: (%s) %s", pSmtp->bFatal ? "fatal" : "retry",
e6c27d3… drh 1045 pSmtp->zErr);
e6c27d3… drh 1046 }
fc5c7d2… drh 1047 email_header_to_free(nTo, azTo);
fc5c7d2… drh 1048 }
fc5c7d2… drh 1049 }else if( strcmp(p->zDest, "stdout")==0 ){
fc5c7d2… drh 1050 char **azTo = 0;
fc5c7d2… drh 1051 int nTo = 0;
fc5c7d2… drh 1052 int i;
fc5c7d2… drh 1053 email_header_to(pHdr, &nTo, &azTo);
fc5c7d2… drh 1054 for(i=0; i<nTo; i++){
fc5c7d2… drh 1055 fossil_print("X-To-Test-%d: [%s]\r\n", i, azTo[i]);
fc5c7d2… drh 1056 }
fc5c7d2… drh 1057 email_header_to_free(nTo, azTo);
fc5c7d2… drh 1058 blob_add_final_newline(&all);
fc5c7d2… drh 1059 fossil_print("%s", blob_str(&all));
fc5c7d2… drh 1060 }
fc5c7d2… drh 1061 blob_reset(&all);
fc5c7d2… drh 1062 }
fc5c7d2… drh 1063
fc5c7d2… drh 1064 /*
a0ae0c9… drh 1065 ** SETTING: email-url width=40
7e993c7… drh 1066 ** This is the main URL used to access the repository for cloning or
7e993c7… drh 1067 ** syncing or for operating the web interface. It is also
7e993c7… drh 1068 ** the basename for hyperlinks included in email alert text.
7e993c7… drh 1069 ** Omit the trailing "/". If the repository is not intended to be
7e993c7… drh 1070 ** a long-running server and will not be sending email notifications,
7e993c7… drh 1071 ** then leave this setting blank.
a0ae0c9… drh 1072 */
a0ae0c9… drh 1073 /*
a0ae0c9… drh 1074 ** SETTING: email-admin width=40
e0576ea… stephan 1075 ** This is the email address for the human administrator for the system.
a0ae0c9… drh 1076 ** Abuse and trouble reports and password reset requests are send here.
a0ae0c9… drh 1077 */
a0ae0c9… drh 1078 /*
a0ae0c9… drh 1079 ** SETTING: email-subname width=16
a0ae0c9… drh 1080 ** This is a short name used to identifies the repository in the Subject:
a0ae0c9… drh 1081 ** line of email alerts. Traditionally this name is included in square
a0ae0c9… drh 1082 ** brackets. Examples: "[fossil-src]", "[sqlite-src]".
a0ae0c9… drh 1083 */
a0ae0c9… drh 1084 /*
34d45c5… drh 1085 ** SETTING: email-renew-interval width=16
34d45c5… drh 1086 ** If this setting as an integer N that is 14 or greater then email
7ae1f31… stephan 1087 ** notification is suspended for subscriptions that have a "last contact
34d45c5… drh 1088 ** time" of more than N days ago. The "last contact time" is recorded
34d45c5… drh 1089 ** in the SUBSCRIBER.LASTCONTACT entry of the database. Logging in,
34d45c5… drh 1090 ** sending a forum post, editing a wiki page, changing subscription settings
34d45c5… drh 1091 ** at /alerts, or visiting /renew all update the last contact time.
34d45c5… drh 1092 ** If this setting is not an integer value or is less than 14 or undefined,
34d45c5… drh 1093 ** then subscriptions never expire.
34d45c5… drh 1094 */
34d45c5… drh 1095 /* X-VARIABLE: email-renew-warning
34d45c5… drh 1096 ** X-VARIABLE: email-renew-cutoff
34d45c5… drh 1097 **
34d45c5… drh 1098 ** These CONFIG table entries are not considered "settings" since their
34d45c5… drh 1099 ** values are computed and updated automatically.
34d45c5… drh 1100 **
34d45c5… drh 1101 ** email-renew-cutoff is the lastContact cutoff for subscription. It
34d45c5… drh 1102 ** is measured in days since 1970-01-01. If The lastContact time for
34d45c5… drh 1103 ** a subscription is less than email-renew-cutoff, then now new emails
34d45c5… drh 1104 ** are sent to the subscriber.
34d45c5… drh 1105 **
34d45c5… drh 1106 ** email-renew-warning is the time (in days since 1970-01-01) when the
34d45c5… drh 1107 ** last batch of "your subscription is about to expire" emails were
34d45c5… drh 1108 ** sent out.
34d45c5… drh 1109 **
e0576ea… stephan 1110 ** email-renew-cutoff is normally 7 days behind email-renew-warning.
34d45c5… drh 1111 */
34d45c5… drh 1112 /*
f741baa… drh 1113 ** SETTING: email-send-method width=5 default=off sensitive
fc5c7d2… drh 1114 ** Determine the method used to send email. Allowed values are
fc5c7d2… drh 1115 ** "off", "relay", "pipe", "dir", "db", and "stdout". The "off" value
fc5c7d2… drh 1116 ** means no email is ever sent. The "relay" value means emails are sent
fc5c7d2… drh 1117 ** to an Mail Sending Agent using SMTP located at email-send-relayhost.
e0576ea… stephan 1118 ** The "pipe" value means email messages are piped into a command
fc5c7d2… drh 1119 ** determined by the email-send-command setting. The "dir" value means
fc5c7d2… drh 1120 ** emails are written to individual files in a directory determined
fc5c7d2… drh 1121 ** by the email-send-dir setting. The "db" value means that emails
fc5c7d2… drh 1122 ** are added to an SQLite database named by the* email-send-db setting.
fc5c7d2… drh 1123 ** The "stdout" value writes email text to standard output, for debugging.
fc5c7d2… drh 1124 */
fc5c7d2… drh 1125 /*
f741baa… drh 1126 ** SETTING: email-send-command width=40 sensitive
fc5c7d2… drh 1127 ** This is a command to which outbound email content is piped when the
fc5c7d2… drh 1128 ** email-send-method is set to "pipe". The command must extract
fc5c7d2… drh 1129 ** recipient, sender, subject, and all other relevant information
fc5c7d2… drh 1130 ** from the email header.
fc5c7d2… drh 1131 */
fc5c7d2… drh 1132 /*
f741baa… drh 1133 ** SETTING: email-send-dir width=40 sensitive
fc5c7d2… drh 1134 ** This is a directory into which outbound emails are written as individual
fc5c7d2… drh 1135 ** files if the email-send-method is set to "dir".
fc5c7d2… drh 1136 */
fc5c7d2… drh 1137 /*
f741baa… drh 1138 ** SETTING: email-send-db width=40 sensitive
fc5c7d2… drh 1139 ** This is an SQLite database file into which outbound emails are written
fc5c7d2… drh 1140 ** if the email-send-method is set to "db".
fc5c7d2… drh 1141 */
fc5c7d2… drh 1142 /*
fc5c7d2… drh 1143 ** SETTING: email-self width=40
fc5c7d2… drh 1144 ** This is the email address for the repository. Outbound emails add
fc5c7d2… drh 1145 ** this email address as the "From:" field.
fc5c7d2… drh 1146 */
fc5c7d2… drh 1147 /*
e50c362… drh 1148 ** SETTING: email-listid width=40
e50c362… drh 1149 ** If this setting is not an empty string, then it becomes the argument to
e50c362… drh 1150 ** a "List-ID:" header that is added to all out-bound notification emails.
0151018… stephan 1151 ** A list ID is required for the generation of unsubscribe links in
0151018… stephan 1152 ** notifications.
e50c362… drh 1153 */
e50c362… drh 1154 /*
8266b5b… drh 1155 ** SETTING: email-send-relayhost width=40 sensitive default=127.0.0.1
fc5c7d2… drh 1156 ** This is the hostname and TCP port to which output email messages
fc5c7d2… drh 1157 ** are sent when email-send-method is "relay". There should be an
fc5c7d2… drh 1158 ** SMTP server configured as a Mail Submission Agent listening on the
fc5c7d2… drh 1159 ** designated host and port and all times.
fc5c7d2… drh 1160 */
fc5c7d2… drh 1161
fc5c7d2… drh 1162
fc5c7d2… drh 1163 /*
3e7c7e2… drh 1164 ** COMMAND: alerts* abbrv-subcom
e0576ea… stephan 1165 **
fc5c7d2… drh 1166 ** Usage: %fossil alerts SUBCOMMAND ARGS...
fc5c7d2… drh 1167 **
fc5c7d2… drh 1168 ** Subcommands:
fc5c7d2… drh 1169 **
fc5c7d2… drh 1170 ** pending Show all pending alerts. Useful for debugging.
fc5c7d2… drh 1171 **
fc5c7d2… drh 1172 ** reset Hard reset of all email notification tables
fc5c7d2… drh 1173 ** in the repository. This erases all subscription
fc5c7d2… drh 1174 ** information. ** Use with extreme care **
fc5c7d2… drh 1175 **
fc5c7d2… drh 1176 ** send Compose and send pending email alerts.
fc5c7d2… drh 1177 ** Some installations may want to do this via
fc5c7d2… drh 1178 ** a cron-job to make sure alerts are sent
fc5c7d2… drh 1179 ** in a timely manner.
fc5c7d2… drh 1180 **
2512d2d… km 1181 ** Options:
fc5c7d2… drh 1182 ** --digest Send digests
34d45c5… drh 1183 ** --renewal Send subscription renewal
34d45c5… drh 1184 ** notices
fc5c7d2… drh 1185 ** --test Write to standard output
fc5c7d2… drh 1186 **
fc5c7d2… drh 1187 ** settings [NAME VALUE] With no arguments, list all email settings.
fc5c7d2… drh 1188 ** Or change the value of a single email setting.
fc5c7d2… drh 1189 **
fc5c7d2… drh 1190 ** status Report on the status of the email alert
fc5c7d2… drh 1191 ** subsystem
fc5c7d2… drh 1192 **
34d45c5… drh 1193 ** subscribers [PATTERN] List all subscribers matching PATTERN. Either
34d45c5… drh 1194 ** LIKE or GLOB wildcards can be used in PATTERN.
fc5c7d2… drh 1195 **
fc5c7d2… drh 1196 ** test-message TO [OPTS] Send a single email message using whatever
fc5c7d2… drh 1197 ** email sending mechanism is currently configured.
fc5c7d2… drh 1198 ** Use this for testing the email notification
2512d2d… km 1199 ** configuration.
fc5c7d2… drh 1200 **
2512d2d… km 1201 ** Options:
34d45c5… drh 1202 ** --body FILENAME Content from FILENAME
34d45c5… drh 1203 ** --smtp-trace Trace SMTP processing
34d45c5… drh 1204 ** --stdout Send msg to stdout
34d45c5… drh 1205 ** -S|--subject SUBJECT Message "subject:"
fc5c7d2… drh 1206 **
fc5c7d2… drh 1207 ** unsubscribe EMAIL Remove a single subscriber with the given EMAIL.
fc5c7d2… drh 1208 */
fc5c7d2… drh 1209 void alert_cmd(void){
fc5c7d2… drh 1210 const char *zCmd;
fc5c7d2… drh 1211 int nCmd;
fc5c7d2… drh 1212 db_find_and_open_repository(0, 0);
fc5c7d2… drh 1213 alert_schema(0);
fc5c7d2… drh 1214 zCmd = g.argc>=3 ? g.argv[2] : "x";
fc5c7d2… drh 1215 nCmd = (int)strlen(zCmd);
fc5c7d2… drh 1216 if( strncmp(zCmd, "pending", nCmd)==0 ){
fc5c7d2… drh 1217 Stmt q;
fc5c7d2… drh 1218 verify_all_options();
fc5c7d2… drh 1219 if( g.argc!=3 ) usage("pending");
fc5c7d2… drh 1220 db_prepare(&q,"SELECT eventid, sentSep, sentDigest, sentMod"
fc5c7d2… drh 1221 " FROM pending_alert");
fc5c7d2… drh 1222 while( db_step(&q)==SQLITE_ROW ){
fc5c7d2… drh 1223 fossil_print("%10s %7s %10s %7s\n",
fc5c7d2… drh 1224 db_column_text(&q,0),
fc5c7d2… drh 1225 db_column_int(&q,1) ? "sentSep" : "",
fc5c7d2… drh 1226 db_column_int(&q,2) ? "sentDigest" : "",
fc5c7d2… drh 1227 db_column_int(&q,3) ? "sentMod" : "");
fc5c7d2… drh 1228 }
fc5c7d2… drh 1229 db_finalize(&q);
fc5c7d2… drh 1230 }else
fc5c7d2… drh 1231 if( strncmp(zCmd, "reset", nCmd)==0 ){
fc5c7d2… drh 1232 int c;
fc5c7d2… drh 1233 int bForce = find_option("force","f",0)!=0;
fc5c7d2… drh 1234 verify_all_options();
fc5c7d2… drh 1235 if( bForce ){
fc5c7d2… drh 1236 c = 'y';
fc5c7d2… drh 1237 }else{
fc5c7d2… drh 1238 Blob yn;
fc5c7d2… drh 1239 fossil_print(
fc5c7d2… drh 1240 "This will erase all content in the repository tables, thus\n"
fc5c7d2… drh 1241 "deleting all subscriber information. The information will be\n"
fc5c7d2… drh 1242 "unrecoverable.\n");
fc5c7d2… drh 1243 prompt_user("Continue? (y/N) ", &yn);
fc5c7d2… drh 1244 c = blob_str(&yn)[0];
fc5c7d2… drh 1245 blob_reset(&yn);
fc5c7d2… drh 1246 }
fc5c7d2… drh 1247 if( c=='y' ){
169ba8d… drh 1248 alert_drop_trigger();
fc5c7d2… drh 1249 db_multi_exec(
fc5c7d2… drh 1250 "DROP TABLE IF EXISTS subscriber;\n"
fc5c7d2… drh 1251 "DROP TABLE IF EXISTS pending_alert;\n"
fc5c7d2… drh 1252 "DROP TABLE IF EXISTS alert_bounce;\n"
fc5c7d2… drh 1253 /* Legacy */
fc5c7d2… drh 1254 "DROP TABLE IF EXISTS alert_pending;\n"
fc5c7d2… drh 1255 "DROP TABLE IF EXISTS subscription;\n"
fc5c7d2… drh 1256 );
fc5c7d2… drh 1257 alert_schema(0);
fc5c7d2… drh 1258 }
fc5c7d2… drh 1259 }else
fc5c7d2… drh 1260 if( strncmp(zCmd, "send", nCmd)==0 ){
fc5c7d2… drh 1261 u32 eFlags = 0;
fc5c7d2… drh 1262 if( find_option("digest",0,0)!=0 ) eFlags |= SENDALERT_DIGEST;
34d45c5… drh 1263 if( find_option("renewal",0,0)!=0 ) eFlags |= SENDALERT_RENEWAL;
fc5c7d2… drh 1264 if( find_option("test",0,0)!=0 ){
fc5c7d2… drh 1265 eFlags |= SENDALERT_PRESERVE|SENDALERT_STDOUT;
fc5c7d2… drh 1266 }
fc5c7d2… drh 1267 verify_all_options();
fc5c7d2… drh 1268 alert_send_alerts(eFlags);
fc5c7d2… drh 1269 }else
fc5c7d2… drh 1270 if( strncmp(zCmd, "settings", nCmd)==0 ){
fc5c7d2… drh 1271 int isGlobal = find_option("global",0,0)!=0;
fc5c7d2… drh 1272 int nSetting;
fc5c7d2… drh 1273 const Setting *pSetting = setting_info(&nSetting);
fc5c7d2… drh 1274 db_open_config(1, 0);
fc5c7d2… drh 1275 verify_all_options();
fc5c7d2… drh 1276 if( g.argc!=3 && g.argc!=5 ) usage("setting [NAME VALUE]");
fc5c7d2… drh 1277 if( g.argc==5 ){
fc5c7d2… drh 1278 const char *zLabel = g.argv[3];
fc5c7d2… drh 1279 if( strncmp(zLabel, "email-", 6)!=0
fc5c7d2… drh 1280 || (pSetting = db_find_setting(zLabel, 1))==0 ){
fc5c7d2… drh 1281 fossil_fatal("not a valid email setting: \"%s\"", zLabel);
fc5c7d2… drh 1282 }
0a5d0e1… drh 1283 db_set(pSetting->name/*works-like:""*/, g.argv[4], isGlobal);
fc5c7d2… drh 1284 g.argc = 3;
fc5c7d2… drh 1285 }
fc5c7d2… drh 1286 pSetting = setting_info(&nSetting);
fc5c7d2… drh 1287 for(; nSetting>0; nSetting--, pSetting++ ){
fc5c7d2… drh 1288 if( strncmp(pSetting->name,"email-",6)!=0 ) continue;
8f4aedc… drh 1289 print_setting(pSetting, 0, 0);
fc5c7d2… drh 1290 }
fc5c7d2… drh 1291 }else
fc5c7d2… drh 1292 if( strncmp(zCmd, "status", nCmd)==0 ){
34d45c5… drh 1293 Stmt q;
34d45c5… drh 1294 int iCutoff;
fc5c7d2… drh 1295 int nSetting, n;
fc5c7d2… drh 1296 static const char *zFmt = "%-29s %d\n";
fc5c7d2… drh 1297 const Setting *pSetting = setting_info(&nSetting);
fc5c7d2… drh 1298 db_open_config(1, 0);
fc5c7d2… drh 1299 verify_all_options();
fc5c7d2… drh 1300 if( g.argc!=3 ) usage("status");
fc5c7d2… drh 1301 pSetting = setting_info(&nSetting);
fc5c7d2… drh 1302 for(; nSetting>0; nSetting--, pSetting++ ){
fc5c7d2… drh 1303 if( strncmp(pSetting->name,"email-",6)!=0 ) continue;
8f4aedc… drh 1304 print_setting(pSetting, 0, 0);
fc5c7d2… drh 1305 }
fc5c7d2… drh 1306 n = db_int(0,"SELECT count(*) FROM pending_alert WHERE NOT sentSep");
fc5c7d2… drh 1307 fossil_print(zFmt/*works-like:"%s%d"*/, "pending-alerts", n);
fc5c7d2… drh 1308 n = db_int(0,"SELECT count(*) FROM pending_alert WHERE NOT sentDigest");
fc5c7d2… drh 1309 fossil_print(zFmt/*works-like:"%s%d"*/, "pending-digest-alerts", n);
34d45c5… drh 1310 db_prepare(&q,
34d45c5… drh 1311 "SELECT"
34d45c5… drh 1312 " name,"
34d45c5… drh 1313 " value,"
34d45c5… drh 1314 " now()/86400-value,"
34d45c5… drh 1315 " date(value*86400,'unixepoch')"
34d45c5… drh 1316 " FROM repository.config"
34d45c5… drh 1317 " WHERE name in ('email-renew-warning','email-renew-cutoff');");
34d45c5… drh 1318 while( db_step(&q)==SQLITE_ROW ){
34d45c5… drh 1319 fossil_print("%-29s %-6d (%d days ago on %s)\n",
34d45c5… drh 1320 db_column_text(&q, 0),
34d45c5… drh 1321 db_column_int(&q, 1),
34d45c5… drh 1322 db_column_int(&q, 2),
34d45c5… drh 1323 db_column_text(&q, 3));
34d45c5… drh 1324 }
34d45c5… drh 1325 db_finalize(&q);
fc5c7d2… drh 1326 n = db_int(0,"SELECT count(*) FROM subscriber");
fc5c7d2… drh 1327 fossil_print(zFmt/*works-like:"%s%d"*/, "total-subscribers", n);
34d45c5… drh 1328 iCutoff = db_get_int("email-renew-cutoff", 0);
fc5c7d2… drh 1329 n = db_int(0, "SELECT count(*) FROM subscriber WHERE sverified"
34d45c5… drh 1330 " AND NOT sdonotcall AND length(ssub)>1"
34d45c5… drh 1331 " AND lastContact>=%d", iCutoff);
fc5c7d2… drh 1332 fossil_print(zFmt/*works-like:"%s%d"*/, "active-subscribers", n);
fc5c7d2… drh 1333 }else
fc5c7d2… drh 1334 if( strncmp(zCmd, "subscribers", nCmd)==0 ){
fc5c7d2… drh 1335 Stmt q;
fc5c7d2… drh 1336 verify_all_options();
fc5c7d2… drh 1337 if( g.argc!=3 && g.argc!=4 ) usage("subscribers [PATTERN]");
fc5c7d2… drh 1338 if( g.argc==4 ){
fc5c7d2… drh 1339 char *zPattern = g.argv[3];
fc5c7d2… drh 1340 db_prepare(&q,
fc5c7d2… drh 1341 "SELECT semail FROM subscriber"
fc5c7d2… drh 1342 " WHERE semail LIKE '%%%q%%' OR suname LIKE '%%%q%%'"
fc5c7d2… drh 1343 " OR semail GLOB '*%q*' or suname GLOB '*%q*'"
fc5c7d2… drh 1344 " ORDER BY semail",
fc5c7d2… drh 1345 zPattern, zPattern, zPattern, zPattern);
fc5c7d2… drh 1346 }else{
fc5c7d2… drh 1347 db_prepare(&q,
fc5c7d2… drh 1348 "SELECT semail FROM subscriber"
fc5c7d2… drh 1349 " ORDER BY semail");
fc5c7d2… drh 1350 }
fc5c7d2… drh 1351 while( db_step(&q)==SQLITE_ROW ){
fc5c7d2… drh 1352 fossil_print("%s\n", db_column_text(&q, 0));
fc5c7d2… drh 1353 }
fc5c7d2… drh 1354 db_finalize(&q);
fc5c7d2… drh 1355 }else
fc5c7d2… drh 1356 if( strncmp(zCmd, "test-message", nCmd)==0 ){
fc5c7d2… drh 1357 Blob prompt, body, hdr;
fc5c7d2… drh 1358 const char *zDest = find_option("stdout",0,0)!=0 ? "stdout" : 0;
fc5c7d2… drh 1359 int i;
fc5c7d2… drh 1360 u32 mFlags = ALERT_IMMEDIATE_FAIL;
fc5c7d2… drh 1361 const char *zSubject = find_option("subject", "S", 1);
fc5c7d2… drh 1362 const char *zSource = find_option("body", 0, 1);
fc5c7d2… drh 1363 AlertSender *pSender;
fc5c7d2… drh 1364 if( find_option("smtp-trace",0,0)!=0 ) mFlags |= ALERT_TRACE;
fc5c7d2… drh 1365 verify_all_options();
fc5c7d2… drh 1366 blob_init(&prompt, 0, 0);
fc5c7d2… drh 1367 blob_init(&body, 0, 0);
fc5c7d2… drh 1368 blob_init(&hdr, 0, 0);
fc5c7d2… drh 1369 blob_appendf(&hdr,"To: ");
fc5c7d2… drh 1370 for(i=3; i<g.argc; i++){
fc5c7d2… drh 1371 if( i>3 ) blob_append(&hdr, ", ", 2);
fc5c7d2… drh 1372 blob_appendf(&hdr, "<%s>", g.argv[i]);
fc5c7d2… drh 1373 }
fc5c7d2… drh 1374 blob_append(&hdr,"\r\n",2);
fc5c7d2… drh 1375 if( zSubject==0 ) zSubject = "fossil alerts test-message";
fc5c7d2… drh 1376 blob_appendf(&hdr, "Subject: %s\r\n", zSubject);
fc5c7d2… drh 1377 if( zSource ){
fc5c7d2… drh 1378 blob_read_from_file(&body, zSource, ExtFILE);
fc5c7d2… drh 1379 }else{
fc5c7d2… drh 1380 prompt_for_user_comment(&body, &prompt);
fc5c7d2… drh 1381 }
fc5c7d2… drh 1382 blob_add_final_newline(&body);
fc5c7d2… drh 1383 pSender = alert_sender_new(zDest, mFlags);
fc5c7d2… drh 1384 alert_send(pSender, &hdr, &body, 0);
fc5c7d2… drh 1385 alert_sender_free(pSender);
fc5c7d2… drh 1386 blob_reset(&hdr);
fc5c7d2… drh 1387 blob_reset(&body);
fc5c7d2… drh 1388 blob_reset(&prompt);
fc5c7d2… drh 1389 }else
fc5c7d2… drh 1390 if( strncmp(zCmd, "unsubscribe", nCmd)==0 ){
fc5c7d2… drh 1391 verify_all_options();
fc5c7d2… drh 1392 if( g.argc!=4 ) usage("unsubscribe EMAIL");
fc5c7d2… drh 1393 db_multi_exec(
fc5c7d2… drh 1394 "DELETE FROM subscriber WHERE semail=%Q", g.argv[3]);
fc5c7d2… drh 1395 }else
fc5c7d2… drh 1396 {
fc5c7d2… drh 1397 usage("pending|reset|send|setting|status|"
fc5c7d2… drh 1398 "subscribers|test-message|unsubscribe");
fc5c7d2… drh 1399 }
fc5c7d2… drh 1400 }
fc5c7d2… drh 1401
fc5c7d2… drh 1402 /*
fc5c7d2… drh 1403 ** Do error checking on a submitted subscription form. Return TRUE
fc5c7d2… drh 1404 ** if the submission is valid. Return false if any problems are seen.
fc5c7d2… drh 1405 */
fc5c7d2… drh 1406 static int subscribe_error_check(
fc5c7d2… drh 1407 int *peErr, /* Type of error */
fc5c7d2… drh 1408 char **pzErr, /* Error message text */
fc5c7d2… drh 1409 int needCaptcha /* True if captcha check needed */
fc5c7d2… drh 1410 ){
fc5c7d2… drh 1411 const char *zEAddr;
fc5c7d2… drh 1412 int i, j, n;
fc5c7d2… drh 1413 char c;
fc5c7d2… drh 1414
fc5c7d2… drh 1415 *peErr = 0;
fc5c7d2… drh 1416 *pzErr = 0;
fc5c7d2… drh 1417
c00e912… drh 1418 /* Verify the captcha first */
c00e912… drh 1419 if( needCaptcha ){
c00e912… drh 1420 if( !captcha_is_correct(1) ){
c00e912… drh 1421 *peErr = 2;
c00e912… drh 1422 *pzErr = mprintf("incorrect security code");
c00e912… drh 1423 return 0;
c00e912… drh 1424 }
c00e912… drh 1425 }
c00e912… drh 1426
fc5c7d2… drh 1427 /* Check the validity of the email address.
fc5c7d2… drh 1428 **
fc5c7d2… drh 1429 ** (1) Exactly one '@' character.
fc5c7d2… drh 1430 ** (2) No other characters besides [a-zA-Z0-9._+-]
fc5c7d2… drh 1431 **
fc5c7d2… drh 1432 ** The local part is currently more restrictive than RFC 5322 allows:
fc5c7d2… drh 1433 ** https://stackoverflow.com/a/2049510/142454 We will expand this as
fc5c7d2… drh 1434 ** necessary.
fc5c7d2… drh 1435 */
fc5c7d2… drh 1436 zEAddr = P("e");
c00e912… drh 1437 if( zEAddr==0 ){
c00e912… drh 1438 *peErr = 1;
c00e912… drh 1439 *pzErr = mprintf("required");
c00e912… drh 1440 return 0;
c00e912… drh 1441 }
fc5c7d2… drh 1442 for(i=j=n=0; (c = zEAddr[i])!=0; i++){
fc5c7d2… drh 1443 if( c=='@' ){
fc5c7d2… drh 1444 n = i;
fc5c7d2… drh 1445 j++;
fc5c7d2… drh 1446 continue;
fc5c7d2… drh 1447 }
fc5c7d2… drh 1448 if( !fossil_isalnum(c) && c!='.' && c!='_' && c!='-' && c!='+' ){
fc5c7d2… drh 1449 *peErr = 1;
fc5c7d2… drh 1450 *pzErr = mprintf("illegal character in email address: 0x%x '%c'",
fc5c7d2… drh 1451 c, c);
fc5c7d2… drh 1452 return 0;
fc5c7d2… drh 1453 }
fc5c7d2… drh 1454 }
fc5c7d2… drh 1455 if( j!=1 ){
fc5c7d2… drh 1456 *peErr = 1;
fc5c7d2… drh 1457 *pzErr = mprintf("email address should contain exactly one '@'");
fc5c7d2… drh 1458 return 0;
fc5c7d2… drh 1459 }
fc5c7d2… drh 1460 if( n<1 ){
fc5c7d2… drh 1461 *peErr = 1;
fc5c7d2… drh 1462 *pzErr = mprintf("name missing before '@' in email address");
fc5c7d2… drh 1463 return 0;
fc5c7d2… drh 1464 }
fc5c7d2… drh 1465 if( n>i-5 ){
fc5c7d2… drh 1466 *peErr = 1;
fc5c7d2… drh 1467 *pzErr = mprintf("email domain too short");
fc5c7d2… drh 1468 return 0;
fc5c7d2… drh 1469 }
fc5c7d2… drh 1470
c00e912… drh 1471 if( authorized_subscription_email(zEAddr)==0 ){
c00e912… drh 1472 *peErr = 1;
c00e912… drh 1473 *pzErr = mprintf("not an authorized email address");
fc5c7d2… drh 1474 return 0;
fc5c7d2… drh 1475 }
fc5c7d2… drh 1476
fc5c7d2… drh 1477 /* Check to make sure the email address is available for reuse */
fc5c7d2… drh 1478 if( db_exists("SELECT 1 FROM subscriber WHERE semail=%Q", zEAddr) ){
fc5c7d2… drh 1479 *peErr = 1;
fc5c7d2… drh 1480 *pzErr = mprintf("this email address is used by someone else");
fc5c7d2… drh 1481 return 0;
fc5c7d2… drh 1482 }
fc5c7d2… drh 1483
fc5c7d2… drh 1484 /* If we reach this point, all is well */
fc5c7d2… drh 1485 return 1;
fc5c7d2… drh 1486 }
fc5c7d2… drh 1487
fc5c7d2… drh 1488 /*
fc5c7d2… drh 1489 ** Text of email message sent in order to confirm a subscription.
fc5c7d2… drh 1490 */
275da70… danield 1491 static const char zConfirmMsg[] =
fc5c7d2… drh 1492 @ Someone has signed you up for email alerts on the Fossil repository
fc5c7d2… drh 1493 @ at %s.
fc5c7d2… drh 1494 @
fc5c7d2… drh 1495 @ To confirm your subscription and begin receiving alerts, click on
fc5c7d2… drh 1496 @ the following hyperlink:
fc5c7d2… drh 1497 @
fc5c7d2… drh 1498 @ %s/alerts/%s
fc5c7d2… drh 1499 @
fc5c7d2… drh 1500 @ Save the hyperlink above! You can reuse this same hyperlink to
fc5c7d2… drh 1501 @ unsubscribe or to change the kinds of alerts you receive.
fc5c7d2… drh 1502 @
fc5c7d2… drh 1503 @ If you do not want to subscribe, you can simply ignore this message.
fc5c7d2… drh 1504 @ You will not be contacted again.
fc5c7d2… drh 1505 @
fc5c7d2… drh 1506 ;
fc5c7d2… drh 1507
fc5c7d2… drh 1508 /*
fc5c7d2… drh 1509 ** Append the text of an email confirmation message to the given
fc5c7d2… drh 1510 ** Blob. The security code is in zCode.
fc5c7d2… drh 1511 */
fc5c7d2… drh 1512 void alert_append_confirmation_message(Blob *pMsg, const char *zCode){
fc5c7d2… drh 1513 blob_appendf(pMsg, zConfirmMsg/*works-like:"%s%s%s"*/,
fc5c7d2… drh 1514 g.zBaseURL, g.zBaseURL, zCode);
fc5c7d2… drh 1515 }
fc5c7d2… drh 1516
fc5c7d2… drh 1517 /*
fc5c7d2… drh 1518 ** WEBPAGE: subscribe
fc5c7d2… drh 1519 **
fc5c7d2… drh 1520 ** Allow users to subscribe to email notifications.
fc5c7d2… drh 1521 **
fc5c7d2… drh 1522 ** This page is usually run by users who are not logged in.
fc5c7d2… drh 1523 ** A logged-in user can add email notifications on the /alerts page.
fc5c7d2… drh 1524 ** Access to this page by a logged in user (other than an
fc5c7d2… drh 1525 ** administrator) results in a redirect to the /alerts page.
fc5c7d2… drh 1526 **
fc5c7d2… drh 1527 ** Administrators can visit this page in order to sign up other
fc5c7d2… drh 1528 ** users.
fc5c7d2… drh 1529 **
fc5c7d2… drh 1530 ** The Alerts permission ("7") is required to access this
fc5c7d2… drh 1531 ** page. To allow anonymous passers-by to sign up for email
fc5c7d2… drh 1532 ** notification, set Email-Alerts on user "nobody" or "anonymous".
fc5c7d2… drh 1533 */
fc5c7d2… drh 1534 void subscribe_page(void){
fc5c7d2… drh 1535 int needCaptcha;
11d1233… drh 1536 unsigned int uSeed = 0;
fc5c7d2… drh 1537 const char *zDecoded;
fc5c7d2… drh 1538 char *zCaptcha = 0;
fc5c7d2… drh 1539 char *zErr = 0;
fc5c7d2… drh 1540 int eErr = 0;
fc5c7d2… drh 1541 int di;
fc5c7d2… drh 1542
fc5c7d2… drh 1543 if( alert_webpages_disabled() ) return;
fc5c7d2… drh 1544 login_check_credentials();
fc5c7d2… drh 1545 if( !g.perm.EmailAlert ){
fc5c7d2… drh 1546 login_needed(g.anon.EmailAlert);
fc5c7d2… drh 1547 return;
fc5c7d2… drh 1548 }
fc5c7d2… drh 1549 if( login_is_individual()
fc5c7d2… drh 1550 && db_exists("SELECT 1 FROM subscriber WHERE suname=%Q",g.zLogin)
fc5c7d2… drh 1551 ){
fc5c7d2… drh 1552 /* This person is already signed up for email alerts. Jump
fc5c7d2… drh 1553 ** to the screen that lets them edit their alert preferences.
fc5c7d2… drh 1554 ** Except, administrators can create subscriptions for others so
fc5c7d2… drh 1555 ** do not jump for them.
fc5c7d2… drh 1556 */
fc5c7d2… drh 1557 if( g.perm.Admin ){
fc5c7d2… drh 1558 /* Admins get a link to admin their own account, but they
fc5c7d2… drh 1559 ** stay on this page so that they can create subscriptions
fc5c7d2… drh 1560 ** for other people. */
fc5c7d2… drh 1561 style_submenu_element("My Subscription","%R/alerts");
fc5c7d2… drh 1562 }else{
fc5c7d2… drh 1563 /* Everybody else jumps to the page to administer their own
fc5c7d2… drh 1564 ** account only. */
fc5c7d2… drh 1565 cgi_redirectf("%R/alerts");
fc5c7d2… drh 1566 return;
fc5c7d2… drh 1567 }
fc5c7d2… drh 1568 }
c00e912… drh 1569 if( !g.perm.Admin && !db_get_boolean("anon-subscribe",1) ){
c00e912… drh 1570 register_page();
c00e912… drh 1571 return;
c00e912… drh 1572 }
112c713… drh 1573 style_set_current_feature("alerts");
fc5c7d2… drh 1574 alert_submenu_common();
fc5c7d2… drh 1575 needCaptcha = !login_is_individual();
fc5c7d2… drh 1576 if( P("submit")
920ace1… drh 1577 && cgi_csrf_safe(2)
fc5c7d2… drh 1578 && subscribe_error_check(&eErr,&zErr,needCaptcha)
fc5c7d2… drh 1579 ){
fc5c7d2… drh 1580 /* A validated request for a new subscription has been received. */
fc5c7d2… drh 1581 char ssub[20];
fc5c7d2… drh 1582 const char *zEAddr = P("e");
fc5c7d2… drh 1583 const char *zCode; /* New subscriber code (in hex) */
fc5c7d2… drh 1584 int nsub = 0;
fc5c7d2… drh 1585 const char *suname = PT("suname");
fc5c7d2… drh 1586 if( suname==0 && needCaptcha==0 && !g.perm.Admin ) suname = g.zLogin;
fc5c7d2… drh 1587 if( suname && suname[0]==0 ) suname = 0;
fc5c7d2… drh 1588 if( PB("sa") ) ssub[nsub++] = 'a';
fc5c7d2… drh 1589 if( g.perm.Read && PB("sc") ) ssub[nsub++] = 'c';
fc5c7d2… drh 1590 if( g.perm.RdForum && PB("sf") ) ssub[nsub++] = 'f';
d4361f6… drh 1591 if( g.perm.RdForum && PB("sn") ) ssub[nsub++] = 'n';
d4361f6… drh 1592 if( g.perm.RdForum && PB("sr") ) ssub[nsub++] = 'r';
fc5c7d2… drh 1593 if( g.perm.RdTkt && PB("st") ) ssub[nsub++] = 't';
d96055c… stephan 1594 if( g.perm.Admin && PB("su") ) ssub[nsub++] = 'u';
fc5c7d2… drh 1595 if( g.perm.RdWiki && PB("sw") ) ssub[nsub++] = 'w';
ea81b30… drh 1596 if( g.perm.RdForum && PB("sx") ) ssub[nsub++] = 'x';
fc5c7d2… drh 1597 ssub[nsub] = 0;
8a3dc1a… drh 1598 zCode = db_text(0,
fc5c7d2… drh 1599 "INSERT INTO subscriber(semail,suname,"
d7e10ce… drh 1600 " sverified,sdonotcall,sdigest,ssub,sctime,mtime,smip,lastContact)"
d7e10ce… drh 1601 "VALUES(%Q,%Q,%d,0,%d,%Q,now(),now(),%Q,now()/86400)"
8a3dc1a… drh 1602 "RETURNING hex(subscriberCode);",
fc5c7d2… drh 1603 /* semail */ zEAddr,
fc5c7d2… drh 1604 /* suname */ suname,
fc5c7d2… drh 1605 /* sverified */ needCaptcha==0,
fc5c7d2… drh 1606 /* sdigest */ PB("di"),
fc5c7d2… drh 1607 /* ssub */ ssub,
fc5c7d2… drh 1608 /* smip */ g.zIpAddr
fc5c7d2… drh 1609 );
fc5c7d2… drh 1610 if( !needCaptcha ){
fc5c7d2… drh 1611 /* The new subscription has been added on behalf of a logged-in user.
fc5c7d2… drh 1612 ** No verification is required. Jump immediately to /alerts page.
fc5c7d2… drh 1613 */
15e1529… drh 1614 if( g.perm.Admin ){
15e1529… drh 1615 cgi_redirectf("%R/alerts/%.32s", zCode);
15e1529… drh 1616 }else{
15e1529… drh 1617 cgi_redirectf("%R/alerts");
15e1529… drh 1618 }
fc5c7d2… drh 1619 return;
fc5c7d2… drh 1620 }else{
fc5c7d2… drh 1621 /* We need to send a verification email */
fc5c7d2… drh 1622 Blob hdr, body;
fc5c7d2… drh 1623 AlertSender *pSender = alert_sender_new(0,0);
fc5c7d2… drh 1624 blob_init(&hdr,0,0);
fc5c7d2… drh 1625 blob_init(&body,0,0);
fc5c7d2… drh 1626 blob_appendf(&hdr, "To: <%s>\n", zEAddr);
fc5c7d2… drh 1627 blob_appendf(&hdr, "Subject: Subscription verification\n");
fc5c7d2… drh 1628 alert_append_confirmation_message(&body, zCode);
fc5c7d2… drh 1629 alert_send(pSender, &hdr, &body, 0);
fc5c7d2… drh 1630 style_header("Email Alert Verification");
fc5c7d2… drh 1631 if( pSender->zErr ){
fc5c7d2… drh 1632 @ <h1>Internal Error</h1>
fc5c7d2… drh 1633 @ <p>The following internal error was encountered while trying
fc5c7d2… drh 1634 @ to send the confirmation email:
fc5c7d2… drh 1635 @ <blockquote><pre>
fc5c7d2… drh 1636 @ %h(pSender->zErr)
fc5c7d2… drh 1637 @ </pre></blockquote>
fc5c7d2… drh 1638 }else{
fc5c7d2… drh 1639 @ <p>An email has been sent to "%h(zEAddr)". That email contains a
c00e912… drh 1640 @ hyperlink that you must click to activate your
fc5c7d2… drh 1641 @ subscription.</p>
fc5c7d2… drh 1642 }
fc5c7d2… drh 1643 alert_sender_free(pSender);
112c713… drh 1644 style_finish_page();
fc5c7d2… drh 1645 }
fc5c7d2… drh 1646 return;
fc5c7d2… drh 1647 }
fc5c7d2… drh 1648 style_header("Signup For Email Alerts");
fc5c7d2… drh 1649 if( P("submit")==0 ){
fc5c7d2… drh 1650 /* If this is the first visit to this page (if this HTTP request did not
fc5c7d2… drh 1651 ** come from a prior Submit of the form) then default all of the
fc5c7d2… drh 1652 ** subscription options to "on" */
fc5c7d2… drh 1653 cgi_set_parameter_nocopy("sa","1",1);
fc5c7d2… drh 1654 if( g.perm.Read ) cgi_set_parameter_nocopy("sc","1",1);
fc5c7d2… drh 1655 if( g.perm.RdForum ) cgi_set_parameter_nocopy("sf","1",1);
d4361f6… drh 1656 if( g.perm.RdForum ) cgi_set_parameter_nocopy("sn","1",1);
d4361f6… drh 1657 if( g.perm.RdForum ) cgi_set_parameter_nocopy("sr","1",1);
fc5c7d2… drh 1658 if( g.perm.RdTkt ) cgi_set_parameter_nocopy("st","1",1);
d96055c… stephan 1659 if( g.perm.Admin ) cgi_set_parameter_nocopy("su","1",1);
fc5c7d2… drh 1660 if( g.perm.RdWiki ) cgi_set_parameter_nocopy("sw","1",1);
fc5c7d2… drh 1661 }
fc5c7d2… drh 1662 @ <p>To receive email notifications for changes to this
cdcffc4… stephan 1663 @ repository, fill out the form below and press the "Submit" button.</p>
fc5c7d2… drh 1664 form_begin(0, "%R/subscribe");
fc5c7d2… drh 1665 @ <table class="subscribe">
fc5c7d2… drh 1666 @ <tr>
fc5c7d2… drh 1667 @ <td class="form_label">Email&nbsp;Address:</td>
fc5c7d2… drh 1668 @ <td><input type="text" name="e" value="%h(PD("e",""))" size="30"></td>
27769be… drh 1669 @ <tr>
fc5c7d2… drh 1670 if( eErr==1 ){
27769be… drh 1671 @ <tr><td><td><span class='loginError'>&uarr; %h(zErr)</span></td></tr>
fc5c7d2… drh 1672 }
fc5c7d2… drh 1673 @ </tr>
fc5c7d2… drh 1674 if( needCaptcha ){
c00e912… drh 1675 const char *zInit = "";
c00e912… drh 1676 if( P("captchaseed")!=0 && eErr!=2 ){
c00e912… drh 1677 uSeed = strtoul(P("captchaseed"),0,10);
c00e912… drh 1678 zInit = P("captcha");
c00e912… drh 1679 }else{
c00e912… drh 1680 uSeed = captcha_seed();
c00e912… drh 1681 }
8659d84… drh 1682 zDecoded = captcha_decode(uSeed, 0);
fc5c7d2… drh 1683 zCaptcha = captcha_render(zDecoded);
fc5c7d2… drh 1684 @ <tr>
fc5c7d2… drh 1685 @ <td class="form_label">Security Code:</td>
c00e912… drh 1686 @ <td><input type="text" name="captcha" value="%h(zInit)" size="30">
a584491… drh 1687 captcha_speakit_button(uSeed, "Speak the code");
fc5c7d2… drh 1688 @ <input type="hidden" name="captchaseed" value="%u(uSeed)"></td>
27769be… drh 1689 @ </tr>
fc5c7d2… drh 1690 if( eErr==2 ){
27769be… drh 1691 @ <tr><td><td><span class='loginError'>&uarr; %h(zErr)</span></td></tr>
fc5c7d2… drh 1692 }
fc5c7d2… drh 1693 @ </tr>
fc5c7d2… drh 1694 }
fc5c7d2… drh 1695 if( g.perm.Admin ){
fc5c7d2… drh 1696 @ <tr>
fc5c7d2… drh 1697 @ <td class="form_label">User:</td>
fc5c7d2… drh 1698 @ <td><input type="text" name="suname" value="%h(PD("suname",g.zLogin))" \
fc5c7d2… drh 1699 @ size="30"></td>
27769be… drh 1700 @ </tr>
fc5c7d2… drh 1701 if( eErr==3 ){
27769be… drh 1702 @ <tr><td><td><span class='loginError'>&uarr; %h(zErr)</span></td></tr>
fc5c7d2… drh 1703 }
fc5c7d2… drh 1704 @ </tr>
fc5c7d2… drh 1705 }
fc5c7d2… drh 1706 @ <tr>
fc5c7d2… drh 1707 @ <td class="form_label">Topics:</td>
fc5c7d2… drh 1708 @ <td><label><input type="checkbox" name="sa" %s(PCK("sa"))> \
fc5c7d2… drh 1709 @ Announcements</label><br>
fc5c7d2… drh 1710 if( g.perm.Read ){
fc5c7d2… drh 1711 @ <label><input type="checkbox" name="sc" %s(PCK("sc"))> \
fc5c7d2… drh 1712 @ Check-ins</label><br>
fc5c7d2… drh 1713 }
fc5c7d2… drh 1714 if( g.perm.RdForum ){
fc5c7d2… drh 1715 @ <label><input type="checkbox" name="sf" %s(PCK("sf"))> \
d4361f6… drh 1716 @ All Forum Posts</label><br>
d4361f6… drh 1717 @ <label><input type="checkbox" name="sn" %s(PCK("sn"))> \
d4361f6… drh 1718 @ New Forum Threads</label><br>
d4361f6… drh 1719 @ <label><input type="checkbox" name="sr" %s(PCK("sr"))> \
d4361f6… drh 1720 @ Replies To My Forum Posts</label><br>
ea81b30… drh 1721 @ <label><input type="checkbox" name="sx" %s(PCK("sx"))> \
d4361f6… drh 1722 @ Edits To Forum Posts</label><br>
fc5c7d2… drh 1723 }
fc5c7d2… drh 1724 if( g.perm.RdTkt ){
fc5c7d2… drh 1725 @ <label><input type="checkbox" name="st" %s(PCK("st"))> \
fc5c7d2… drh 1726 @ Ticket changes</label><br>
fc5c7d2… drh 1727 }
fc5c7d2… drh 1728 if( g.perm.RdWiki ){
fc5c7d2… drh 1729 @ <label><input type="checkbox" name="sw" %s(PCK("sw"))> \
fc5c7d2… drh 1730 @ Wiki</label><br>
d96055c… stephan 1731 }
d96055c… stephan 1732 if( g.perm.Admin ){
d96055c… stephan 1733 @ <label><input type="checkbox" name="su" %s(PCK("su"))> \
36f72c0… stephan 1734 @ User permission changes</label>
bca95cb… drh 1735 }
fc5c7d2… drh 1736 di = PB("di");
fc5c7d2… drh 1737 @ </td></tr>
fc5c7d2… drh 1738 @ <tr>
fc5c7d2… drh 1739 @ <td class="form_label">Delivery:</td>
fc5c7d2… drh 1740 @ <td><select size="1" name="di">
fc5c7d2… drh 1741 @ <option value="0" %s(di?"":"selected")>Individual Emails</option>
fc5c7d2… drh 1742 @ <option value="1" %s(di?"selected":"")>Daily Digest</option>
fc5c7d2… drh 1743 @ </select></td>
fc5c7d2… drh 1744 @ </tr>
fc5c7d2… drh 1745 if( g.perm.Admin ){
fc5c7d2… drh 1746 @ <tr>
fc5c7d2… drh 1747 @ <td class="form_label">Admin Options:</td><td>
fc5c7d2… drh 1748 @ <label><input type="checkbox" name="vi" %s(PCK("vi"))> \
fc5c7d2… drh 1749 @ Verified</label><br>
fc5c7d2… drh 1750 @ <label><input type="checkbox" name="dnc" %s(PCK("dnc"))> \
fc5c7d2… drh 1751 @ Do not call</label></td></tr>
fc5c7d2… drh 1752 }
fc5c7d2… drh 1753 @ <tr>
fc5c7d2… drh 1754 @ <td></td>
fc5c7d2… drh 1755 if( needCaptcha && !alert_enabled() ){
fc5c7d2… drh 1756 @ <td><input type="submit" name="submit" value="Submit" disabled>
fc5c7d2… drh 1757 @ (Email current disabled)</td>
fc5c7d2… drh 1758 }else{
fc5c7d2… drh 1759 @ <td><input type="submit" name="submit" value="Submit"></td>
fc5c7d2… drh 1760 }
fc5c7d2… drh 1761 @ </tr>
fc5c7d2… drh 1762 @ </table>
fc5c7d2… drh 1763 if( needCaptcha ){
75c89de… drh 1764 @ <div class="captcha"><table class="captcha"><tr><td><pre class="captcha">
fc5c7d2… drh 1765 @ %h(zCaptcha)
fc5c7d2… drh 1766 @ </pre>
dcf4410… drh 1767 @ Enter the 8 characters above in the "Security Code" box<br/>
fc5c7d2… drh 1768 @ </td></tr></table></div>
fc5c7d2… drh 1769 }
fc5c7d2… drh 1770 @ </form>
fc5c7d2… drh 1771 fossil_free(zErr);
112c713… drh 1772 style_finish_page();
fc5c7d2… drh 1773 }
fc5c7d2… drh 1774
fc5c7d2… drh 1775 /*
fc5c7d2… drh 1776 ** Either shutdown or completely delete a subscription entry given
fc5c7d2… drh 1777 ** by the hex value zName. Then paint a webpage that explains that
fc5c7d2… drh 1778 ** the entry has been removed.
fc5c7d2… drh 1779 */
bca95cb… drh 1780 static void alert_unsubscribe(int sid, int bTotal){
b6b5a7d… drh 1781 const char *zEmail = 0;
b6b5a7d… drh 1782 const char *zLogin = 0;
b6b5a7d… drh 1783 int uid = 0;
b6b5a7d… drh 1784 Stmt q;
b6b5a7d… drh 1785 db_prepare(&q, "SELECT semail, suname FROM subscriber"
b6b5a7d… drh 1786 " WHERE subscriberId=%d", sid);
b6b5a7d… drh 1787 if( db_step(&q)==SQLITE_ROW ){
b6b5a7d… drh 1788 zEmail = db_column_text(&q, 0);
b6b5a7d… drh 1789 zLogin = db_column_text(&q, 1);
b6b5a7d… drh 1790 uid = db_int(0, "SELECT uid FROM user WHERE login=%Q", zLogin);
b6b5a7d… drh 1791 }
112c713… drh 1792 style_set_current_feature("alerts");
fc5c7d2… drh 1793 if( zEmail==0 ){
fc5c7d2… drh 1794 style_header("Unsubscribe Fail");
fc5c7d2… drh 1795 @ <p>Unable to locate a subscriber with the requested key</p>
fc5c7d2… drh 1796 }else{
bca95cb… drh 1797 db_unprotect(PROTECT_READONLY);
bca95cb… drh 1798 if( bTotal ){
bca95cb… drh 1799 /* Completely delete the subscriber */
bca95cb… drh 1800 db_multi_exec(
bca95cb… drh 1801 "DELETE FROM subscriber WHERE subscriberId=%d", sid
bca95cb… drh 1802 );
bca95cb… drh 1803 }else{
bca95cb… drh 1804 /* Keep the subscriber, but turn off all notifications */
bca95cb… drh 1805 db_multi_exec(
bca95cb… drh 1806 "UPDATE subscriber SET ssub='k', mtime=now() WHERE subscriberId=%d",
bca95cb… drh 1807 sid
bca95cb… drh 1808 );
bca95cb… drh 1809 }
bca95cb… drh 1810 db_protect_pop();
fc5c7d2… drh 1811 style_header("Unsubscribed");
4a3909a… drh 1812 @ <p>The "%h(zEmail)" email address has been unsubscribed from all
4a3909a… drh 1813 @ notifications. All subscription records for "%h(zEmail)" have
4a3909a… drh 1814 @ been purged. No further emails will be sent to "%h(zEmail)".</p>
b6b5a7d… drh 1815 if( uid && g.perm.Admin ){
b6b5a7d… drh 1816 @ <p>You may also want to
b6b5a7d… drh 1817 @ <a href="%R/setup_uedit?id=%d(uid)">edit or delete
b6b5a7d… drh 1818 @ the corresponding user "%h(zLogin)"</a></p>
b6b5a7d… drh 1819 }
fc5c7d2… drh 1820 }
b6b5a7d… drh 1821 db_finalize(&q);
112c713… drh 1822 style_finish_page();
fc5c7d2… drh 1823 return;
fc5c7d2… drh 1824 }
fc5c7d2… drh 1825
fc5c7d2… drh 1826 /*
fc5c7d2… drh 1827 ** WEBPAGE: alerts
fc5c7d2… drh 1828 **
fc5c7d2… drh 1829 ** Edit email alert and notification settings.
fc5c7d2… drh 1830 **
15e1529… drh 1831 ** The subscriber is identified in several ways:
15e1529… drh 1832 **
cd06177… drh 1833 ** * The name= query parameter contains the complete subscriberCode.
15e1529… drh 1834 ** This only happens when the user receives a verification
15e1529… drh 1835 ** email and clicks on the link in the email. When a
15e1529… drh 1836 ** compilete subscriberCode is seen on the name= query parameter,
15e1529… drh 1837 ** that constitutes verification of the email address.
15e1529… drh 1838 **
cd06177… drh 1839 ** * The sid= query parameter contains an integer subscriberId.
15e1529… drh 1840 ** This only works for the administrator. It allows the
15e1529… drh 1841 ** administrator to edit any subscription.
e0576ea… stephan 1842 **
cd06177… drh 1843 ** * The user is logged into an account other than "nobody" or
fc5c7d2… drh 1844 ** "anonymous". In that case the notification settings
fc5c7d2… drh 1845 ** associated with that account can be edited without needing
fc5c7d2… drh 1846 ** to know the subscriber code.
15e1529… drh 1847 **
cd06177… drh 1848 ** * The name= query parameter contains a 32-digit prefix of
15e1529… drh 1849 ** subscriber code. (Subscriber codes are normally 64 hex digits
15e1529… drh 1850 ** in length.) This uniquely identifies the subscriber without
15e1529… drh 1851 ** revealing the complete subscriber code, and hence without
15e1529… drh 1852 ** verifying the email address.
54a6f09… drh 1853 */
54a6f09… drh 1854 void alert_page(void){
15e1529… drh 1855 const char *zName = 0; /* Value of the name= query parameter */
15e1529… drh 1856 Stmt q; /* For querying the database */
d96055c… stephan 1857 int sa, sc, sf, st, su, sw, sx; /* Types of notifications requested */
d4361f6… drh 1858 int sn, sr;
15e1529… drh 1859 int sdigest = 0, sdonotcall = 0, sverified = 0; /* Other fields */
15e1529… drh 1860 int isLogin; /* True if logged in as an individual */
15e1529… drh 1861 const char *ssub = 0; /* Subscription flags */
15e1529… drh 1862 const char *semail = 0; /* Email address */
15e1529… drh 1863 const char *smip; /* */
15e1529… drh 1864 const char *suname = 0; /* Corresponding user.login value */
15e1529… drh 1865 const char *mtime; /* */
15e1529… drh 1866 const char *sctime; /* Time subscription created */
15e1529… drh 1867 int eErr = 0; /* Type of error */
15e1529… drh 1868 char *zErr = 0; /* Error message text */
15e1529… drh 1869 int sid = 0; /* Subscriber ID */
15e1529… drh 1870 int nName; /* Length of zName in bytes */
15e1529… drh 1871 char *zHalfCode; /* prefix of subscriberCode */
d7e10ce… drh 1872 int keepAlive = 0; /* True to update the last contact time */
15e1529… drh 1873
c00e912… drh 1874 db_begin_transaction();
c00e912… drh 1875 if( alert_webpages_disabled() ){
c00e912… drh 1876 db_commit_transaction();
15e1529… drh 1877 return;
15e1529… drh 1878 }
c00e912… drh 1879 login_check_credentials();
54a6f09… drh 1880 isLogin = login_is_individual();
15e1529… drh 1881 zName = P("name");
15e1529… drh 1882 nName = zName ? (int)strlen(zName) : 0;
15e1529… drh 1883 if( g.perm.Admin && P("sid")!=0 ){
15e1529… drh 1884 sid = atoi(P("sid"));
54a6f09… drh 1885 }
15e1529… drh 1886 if( sid==0 && nName>=32 ){
15e1529… drh 1887 sid = db_int(0,
15e1529… drh 1888 "SELECT CASE WHEN hex(subscriberCode) LIKE (%Q||'%%')"
15e1529… drh 1889 " THEN subscriberId ELSE 0 END"
15e1529… drh 1890 " FROM subscriber WHERE subscriberCode>=hextoblob(%Q)"
15e1529… drh 1891 " LIMIT 1", zName, zName);
d7e10ce… drh 1892 if( sid ) keepAlive = 1;
15e1529… drh 1893 }
a27c908… drh 1894 if( sid==0 && isLogin && g.perm.EmailAlert ){
15e1529… drh 1895 sid = db_int(0, "SELECT subscriberId FROM subscriber"
15e1529… drh 1896 " WHERE suname=%Q", g.zLogin);
15e1529… drh 1897 }
15e1529… drh 1898 if( sid==0 ){
c00e912… drh 1899 db_commit_transaction();
54a6f09… drh 1900 cgi_redirect("subscribe");
c00e912… drh 1901 /*NOTREACHED*/
54a6f09… drh 1902 }
54a6f09… drh 1903 alert_submenu_common();
920ace1… drh 1904 if( P("submit")!=0 && cgi_csrf_safe(2) ){
32a8d11… drh 1905 char newSsub[10];
54a6f09… drh 1906 int nsub = 0;
54a6f09… drh 1907 Blob update;
32a8d11… drh 1908
32a8d11… drh 1909 sdonotcall = PB("sdonotcall");
32a8d11… drh 1910 sdigest = PB("sdigest");
32a8d11… drh 1911 semail = P("semail");
32a8d11… drh 1912 if( PB("sa") ) newSsub[nsub++] = 'a';
32a8d11… drh 1913 if( g.perm.Read && PB("sc") ) newSsub[nsub++] = 'c';
32a8d11… drh 1914 if( g.perm.RdForum && PB("sf") ) newSsub[nsub++] = 'f';
d4361f6… drh 1915 if( g.perm.RdForum && PB("sn") ) newSsub[nsub++] = 'n';
d4361f6… drh 1916 if( g.perm.RdForum && PB("sr") ) newSsub[nsub++] = 'r';
32a8d11… drh 1917 if( g.perm.RdTkt && PB("st") ) newSsub[nsub++] = 't';
d96055c… stephan 1918 if( g.perm.Admin && PB("su") ) newSsub[nsub++] = 'u';
32a8d11… drh 1919 if( g.perm.RdWiki && PB("sw") ) newSsub[nsub++] = 'w';
e5653a4… drh 1920 if( g.perm.RdForum && PB("sx") ) newSsub[nsub++] = 'x';
32a8d11… drh 1921 newSsub[nsub] = 0;
32a8d11… drh 1922 ssub = newSsub;
54a6f09… drh 1923 blob_init(&update, "UPDATE subscriber SET", -1);
54a6f09… drh 1924 blob_append_sql(&update,
54a6f09… drh 1925 " sdonotcall=%d,"
54a6f09… drh 1926 " sdigest=%d,"
54a6f09… drh 1927 " ssub=%Q,"
d7e10ce… drh 1928 " mtime=now(),"
d7e10ce… drh 1929 " lastContact=now()/86400,"
54a6f09… drh 1930 " smip=%Q",
54a6f09… drh 1931 sdonotcall,
54a6f09… drh 1932 sdigest,
54a6f09… drh 1933 ssub,
54a6f09… drh 1934 g.zIpAddr
54a6f09… drh 1935 );
54a6f09… drh 1936 if( g.perm.Admin ){
32a8d11… drh 1937 suname = PT("suname");
32a8d11… drh 1938 sverified = PB("sverified");
54a6f09… drh 1939 if( suname && suname[0]==0 ) suname = 0;
54a6f09… drh 1940 blob_append_sql(&update,
54a6f09… drh 1941 ", suname=%Q,"
54a6f09… drh 1942 " sverified=%d",
54a6f09… drh 1943 suname,
54a6f09… drh 1944 sverified
54a6f09… drh 1945 );
54a6f09… drh 1946 }
32a8d11… drh 1947 if( isLogin ){
32a8d11… drh 1948 if( semail==0 || email_address_is_valid(semail,0)==0 ){
32a8d11… drh 1949 eErr = 8;
32a8d11… drh 1950 }
32a8d11… drh 1951 blob_append_sql(&update, ", semail=%Q", semail);
54a6f09… drh 1952 }
15e1529… drh 1953 blob_append_sql(&update," WHERE subscriberId=%d", sid);
32a8d11… drh 1954 if( eErr==0 ){
32a8d11… drh 1955 db_exec_sql(blob_str(&update));
32a8d11… drh 1956 ssub = 0;
32a8d11… drh 1957 }
54a6f09… drh 1958 blob_reset(&update);
d7e10ce… drh 1959 }else if( keepAlive ){
db16262… drh 1960 db_unprotect(PROTECT_READONLY);
d7e10ce… drh 1961 db_multi_exec(
d7e10ce… drh 1962 "UPDATE subscriber SET lastContact=now()/86400"
d7e10ce… drh 1963 " WHERE subscriberId=%d", sid
d7e10ce… drh 1964 );
db16262… drh 1965 db_protect_pop();
54a6f09… drh 1966 }
920ace1… drh 1967 if( P("delete")!=0 && cgi_csrf_safe(2) ){
fc5c7d2… drh 1968 if( !PB("dodelete") ){
fc5c7d2… drh 1969 eErr = 9;
fc5c7d2… drh 1970 zErr = mprintf("Select this checkbox and press \"Unsubscribe\" again to"
fc5c7d2… drh 1971 " unsubscribe");
fc5c7d2… drh 1972 }else{
bca95cb… drh 1973 alert_unsubscribe(sid, 1);
c00e912… drh 1974 db_commit_transaction();
e0576ea… stephan 1975 return;
fc5c7d2… drh 1976 }
fc5c7d2… drh 1977 }
112c713… drh 1978 style_set_current_feature("alerts");
32a8d11… drh 1979 style_header("Update Subscription");
fc5c7d2… drh 1980 db_prepare(&q,
fc5c7d2… drh 1981 "SELECT"
fc5c7d2… drh 1982 " semail," /* 0 */
fc5c7d2… drh 1983 " sverified," /* 1 */
fc5c7d2… drh 1984 " sdonotcall," /* 2 */
fc5c7d2… drh 1985 " sdigest," /* 3 */
fc5c7d2… drh 1986 " ssub," /* 4 */
fc5c7d2… drh 1987 " smip," /* 5 */
fc5c7d2… drh 1988 " suname," /* 6 */
fc5c7d2… drh 1989 " datetime(mtime,'unixepoch')," /* 7 */
15e1529… drh 1990 " datetime(sctime,'unixepoch')," /* 8 */
34d45c5… drh 1991 " hex(subscriberCode)," /* 9 */
34d45c5… drh 1992 " date(coalesce(lastContact*86400,mtime),'unixepoch')," /* 10 */
34d45c5… drh 1993 " now()/86400 - coalesce(lastContact,mtime/86400)" /* 11 */
15e1529… drh 1994 " FROM subscriber WHERE subscriberId=%d", sid);
fc5c7d2… drh 1995 if( db_step(&q)!=SQLITE_ROW ){
fc5c7d2… drh 1996 db_finalize(&q);
c00e912… drh 1997 db_commit_transaction();
fc5c7d2… drh 1998 cgi_redirect("subscribe");
c00e912… drh 1999 /*NOTREACHED*/
32a8d11… drh 2000 }
32a8d11… drh 2001 if( ssub==0 ){
32a8d11… drh 2002 semail = db_column_text(&q, 0);
32a8d11… drh 2003 sdonotcall = db_column_int(&q, 2);
32a8d11… drh 2004 sdigest = db_column_int(&q, 3);
32a8d11… drh 2005 ssub = db_column_text(&q, 4);
32a8d11… drh 2006 }
32a8d11… drh 2007 if( suname==0 ){
32a8d11… drh 2008 suname = db_column_text(&q, 6);
32a8d11… drh 2009 sverified = db_column_int(&q, 1);
32a8d11… drh 2010 }
fc5c7d2… drh 2011 sa = strchr(ssub,'a')!=0;
fc5c7d2… drh 2012 sc = strchr(ssub,'c')!=0;
fc5c7d2… drh 2013 sf = strchr(ssub,'f')!=0;
d4361f6… drh 2014 sn = strchr(ssub,'n')!=0;
d4361f6… drh 2015 sr = strchr(ssub,'r')!=0;
fc5c7d2… drh 2016 st = strchr(ssub,'t')!=0;
d96055c… stephan 2017 su = strchr(ssub,'u')!=0;
fc5c7d2… drh 2018 sw = strchr(ssub,'w')!=0;
e5653a4… drh 2019 sx = strchr(ssub,'x')!=0;
fc5c7d2… drh 2020 smip = db_column_text(&q, 5);
fc5c7d2… drh 2021 mtime = db_column_text(&q, 7);
fc5c7d2… drh 2022 sctime = db_column_text(&q, 8);
fc5c7d2… drh 2023 if( !g.perm.Admin && !sverified ){
15e1529… drh 2024 if( nName==64 ){
f33976f… drh 2025 db_unprotect(PROTECT_READONLY);
c00e912… drh 2026 db_multi_exec(
c00e912… drh 2027 "UPDATE subscriber SET sverified=1"
c00e912… drh 2028 " WHERE subscriberCode=hextoblob(%Q)",
15e1529… drh 2029 zName);
f33976f… drh 2030 db_protect_pop();
c00e912… drh 2031 if( db_get_boolean("selfreg-verify",0) ){
c00e912… drh 2032 char *zNewCap = db_get("default-perms","u");
f741baa… drh 2033 db_unprotect(PROTECT_USER);
c00e912… drh 2034 db_multi_exec(
c00e912… drh 2035 "UPDATE user"
c00e912… drh 2036 " SET cap=%Q"
c00e912… drh 2037 " WHERE cap='7' AND login=("
c00e912… drh 2038 " SELECT suname FROM subscriber"
c00e912… drh 2039 " WHERE subscriberCode=hextoblob(%Q))",
c00e912… drh 2040 zNewCap, zName
c00e912… drh 2041 );
f741baa… drh 2042 db_protect_pop();
c00e912… drh 2043 login_set_capabilities(zNewCap, 0);
c00e912… drh 2044 }
15e1529… drh 2045 @ <h1>Your email alert subscription has been verified!</h1>
15e1529… drh 2046 @ <p>Use the form below to update your subscription information.</p>
15e1529… drh 2047 @ <p>Hint: Bookmark this page so that you can more easily update
15e1529… drh 2048 @ your subscription information in the future</p>
15e1529… drh 2049 }else{
15e1529… drh 2050 @ <h2>Your email address is unverified</h2>
15e1529… drh 2051 @ <p>You should have received an email message containing a link
15e1529… drh 2052 @ that you must visit to verify your account. No email notifications
15e1529… drh 2053 @ will be sent until your email address has been verified.</p>
15e1529… drh 2054 }
fc5c7d2… drh 2055 }else{
fc5c7d2… drh 2056 @ <p>Make changes to the email subscription shown below and
fc5c7d2… drh 2057 @ press "Submit".</p>
fc5c7d2… drh 2058 }
fc5c7d2… drh 2059 form_begin(0, "%R/alerts");
15e1529… drh 2060 zHalfCode = db_text("x","SELECT hex(substr(subscriberCode,1,16))"
15e1529… drh 2061 " FROM subscriber WHERE subscriberId=%d", sid);
15e1529… drh 2062 @ <input type="hidden" name="name" value="%h(zHalfCode)">
fc5c7d2… drh 2063 @ <table class="subscribe">
fc5c7d2… drh 2064 @ <tr>
fc5c7d2… drh 2065 @ <td class="form_label">Email&nbsp;Address:</td>
54a6f09… drh 2066 if( isLogin ){
bb05299… drh 2067 @ <td><input type="text" name="semail" value="%h(semail)" size="30">\
32a8d11… drh 2068 if( eErr==8 ){
32a8d11… drh 2069 @ <span class='loginError'>&larr; not a valid email address!</span>
32a8d11… drh 2070 }else if( g.perm.Admin ){
32a8d11… drh 2071 @ &nbsp;&nbsp;<a href="%R/announce?to=%t(semail)">\
32a8d11… drh 2072 @ (Send a message to %h(semail))</a>\
bb05299… drh 2073 }
bb05299… drh 2074 @ </td>
54a6f09… drh 2075 }else{
54a6f09… drh 2076 @ <td>%h(semail)</td>
54a6f09… drh 2077 }
fc5c7d2… drh 2078 @ </tr>
fc5c7d2… drh 2079 if( g.perm.Admin ){
54a6f09… drh 2080 int uid;
fc5c7d2… drh 2081 @ <tr>
fc5c7d2… drh 2082 @ <td class='form_label'>Created:</td>
fc5c7d2… drh 2083 @ <td>%h(sctime)</td>
fc5c7d2… drh 2084 @ </tr>
fc5c7d2… drh 2085 @ <tr>
fc5c7d2… drh 2086 @ <td class='form_label'>Last Modified:</td>
fc5c7d2… drh 2087 @ <td>%h(mtime)</td>
fc5c7d2… drh 2088 @ </tr>
fc5c7d2… drh 2089 @ <tr>
fc5c7d2… drh 2090 @ <td class='form_label'>IP Address:</td>
fc5c7d2… drh 2091 @ <td>%h(smip)</td>
fc5c7d2… drh 2092 @ </tr>
fc5c7d2… drh 2093 @ <tr>
15e1529… drh 2094 @ <td class='form_label'>Subscriber&nbsp;Code:</td>
15e1529… drh 2095 @ <td>%h(db_column_text(&q,9))</td>
15e1529… drh 2096 @ <tr>
34d45c5… drh 2097 @ <tr>
34d45c5… drh 2098 @ <td class='form_label'>Last Contact:</td>
34d45c5… drh 2099 @ <td>%h(db_column_text(&q,10)) &larr; \
34d45c5… drh 2100 @ %,d(db_column_int(&q,11)) days ago</td>
34d45c5… drh 2101 @ </tr>
fc5c7d2… drh 2102 @ <td class="form_label">User:</td>
fc5c7d2… drh 2103 @ <td><input type="text" name="suname" value="%h(suname?suname:"")" \
54a6f09… drh 2104 @ size="30">\
54a6f09… drh 2105 uid = db_int(0, "SELECT uid FROM user WHERE login=%Q", suname);
54a6f09… drh 2106 if( uid ){
54a6f09… drh 2107 @ &nbsp;&nbsp;<a href='%R/setup_uedit?id=%d(uid)'>\
54a6f09… drh 2108 @ (login info for %h(suname))</a>\
54a6f09… drh 2109 }
fc5c7d2… drh 2110 @ </tr>
fc5c7d2… drh 2111 }
fc5c7d2… drh 2112 @ <tr>
fc5c7d2… drh 2113 @ <td class="form_label">Topics:</td>
fc5c7d2… drh 2114 @ <td><label><input type="checkbox" name="sa" %s(sa?"checked":"")>\
fc5c7d2… drh 2115 @ Announcements</label><br>
fc5c7d2… drh 2116 if( g.perm.Read ){
fc5c7d2… drh 2117 @ <label><input type="checkbox" name="sc" %s(sc?"checked":"")>\
fc5c7d2… drh 2118 @ Check-ins</label><br>
fc5c7d2… drh 2119 }
fc5c7d2… drh 2120 if( g.perm.RdForum ){
fc5c7d2… drh 2121 @ <label><input type="checkbox" name="sf" %s(sf?"checked":"")>\
d4361f6… drh 2122 @ All Forum Posts</label><br>
d4361f6… drh 2123 @ <label><input type="checkbox" name="sn" %s(sn?"checked":"")>\
d4361f6… drh 2124 @ New Forum Threads</label><br>
d4361f6… drh 2125 @ <label><input type="checkbox" name="sr" %s(sr?"checked":"")>\
d4361f6… drh 2126 @ Replies To My Posts</label><br>
e5653a4… drh 2127 @ <label><input type="checkbox" name="sx" %s(sx?"checked":"")>\
d4361f6… drh 2128 @ Edits To Forum Posts</label><br>
fc5c7d2… drh 2129 }
fc5c7d2… drh 2130 if( g.perm.RdTkt ){
fc5c7d2… drh 2131 @ <label><input type="checkbox" name="st" %s(st?"checked":"")>\
fc5c7d2… drh 2132 @ Ticket changes</label><br>
fc5c7d2… drh 2133 }
fc5c7d2… drh 2134 if( g.perm.RdWiki ){
fc5c7d2… drh 2135 @ <label><input type="checkbox" name="sw" %s(sw?"checked":"")>\
d96055c… stephan 2136 @ Wiki</label><br>
d96055c… stephan 2137 }
d96055c… stephan 2138 if( g.perm.Admin ){
d96055c… stephan 2139 /* Corner-case bug: if an admin assigns 'u' to a non-admin, that
d96055c… stephan 2140 ** subscription will get removed if the user later edits their
d96055c… stephan 2141 ** subscriptions, as non-admins are not permitted to add that
d96055c… stephan 2142 ** subscription. */
d96055c… stephan 2143 @ <label><input type="checkbox" name="su" %s(su?"checked":"")>\
36f72c0… stephan 2144 @ User permission changes</label>
fc5c7d2… drh 2145 }
fc5c7d2… drh 2146 @ </td></tr>
bca95cb… drh 2147 if( strchr(ssub,'k')!=0 ){
bca95cb… drh 2148 @ <tr><td></td><td>&nbsp;&uarr;&nbsp;
bca95cb… drh 2149 @ Note: User did a one-click unsubscribe</td></tr>
bca95cb… drh 2150 }
fc5c7d2… drh 2151 @ <tr>
fc5c7d2… drh 2152 @ <td class="form_label">Delivery:</td>
fc5c7d2… drh 2153 @ <td><select size="1" name="sdigest">
fc5c7d2… drh 2154 @ <option value="0" %s(sdigest?"":"selected")>Individual Emails</option>
fc5c7d2… drh 2155 @ <option value="1" %s(sdigest?"selected":"")>Daily Digest</option>
fc5c7d2… drh 2156 @ </select></td>
fc5c7d2… drh 2157 @ </tr>
fc5c7d2… drh 2158 if( g.perm.Admin ){
fc5c7d2… drh 2159 @ <tr>
fc5c7d2… drh 2160 @ <td class="form_label">Admin Options:</td><td>
fc5c7d2… drh 2161 @ <label><input type="checkbox" name="sdonotcall" \
54a6f09… drh 2162 @ %s(sdonotcall?"checked":"")> Do not disturb</label><br>
fc5c7d2… drh 2163 @ <label><input type="checkbox" name="sverified" \
fc5c7d2… drh 2164 @ %s(sverified?"checked":"")>\
fc5c7d2… drh 2165 @ Verified</label></td></tr>
fc5c7d2… drh 2166 }
fc5c7d2… drh 2167 if( eErr==9 ){
fc5c7d2… drh 2168 @ <tr>
fc5c7d2… drh 2169 @ <td class="form_label">Verify:</td><td>
fc5c7d2… drh 2170 @ <label><input type="checkbox" name="dodelete">
fc5c7d2… drh 2171 @ Unsubscribe</label>
fc5c7d2… drh 2172 @ <span class="loginError">&larr; %h(zErr)</span>
fc5c7d2… drh 2173 @ </td></tr>
fc5c7d2… drh 2174 }
fc5c7d2… drh 2175 @ <tr>
fc5c7d2… drh 2176 @ <td></td>
fc5c7d2… drh 2177 @ <td><input type="submit" name="submit" value="Submit">
fc5c7d2… drh 2178 @ <input type="submit" name="delete" value="Unsubscribe">
fc5c7d2… drh 2179 @ </tr>
fc5c7d2… drh 2180 @ </table>
fc5c7d2… drh 2181 @ </form>
fc5c7d2… drh 2182 fossil_free(zErr);
fc5c7d2… drh 2183 db_finalize(&q);
112c713… drh 2184 style_finish_page();
c00e912… drh 2185 db_commit_transaction();
c00e912… drh 2186 return;
c00e912… drh 2187 }
c00e912… drh 2188
7b8be20… drh 2189 /*
7b8be20… drh 2190 ** WEBPAGE: renew
7b8be20… drh 2191 **
34d45c5… drh 2192 ** Users visit this page to update the last-contact date on their
34d45c5… drh 2193 ** subscription. The last-contact date is the day that the subscriber
34d45c5… drh 2194 ** last interacted with the repository. If the name= query parameter
34d45c5… drh 2195 ** (or POST parameter) contains a valid subscriber code, then the last-contact
34d45c5… drh 2196 ** subscription associated with that subscriber code is updated to be the
34d45c5… drh 2197 ** current date.
7b8be20… drh 2198 */
7b8be20… drh 2199 void renewal_page(void){
7b8be20… drh 2200 const char *zName = P("name");
7b8be20… drh 2201 int iInterval = db_get_int("email-renew-interval", 0);
7b8be20… drh 2202 Stmt s;
7b8be20… drh 2203 int rc;
7b8be20… drh 2204
7b8be20… drh 2205 style_header("Subscription Renewal");
7b8be20… drh 2206 if( zName==0 || strlen(zName)<4 ){
7b8be20… drh 2207 @ <p>No subscription specified</p>
7b8be20… drh 2208 style_finish_page();
7b8be20… drh 2209 return;
7b8be20… drh 2210 }
7b8be20… drh 2211
7b8be20… drh 2212 if( !db_table_has_column("repository","subscriber","lastContact")
7b8be20… drh 2213 || iInterval<1
7b8be20… drh 2214 ){
7b8be20… drh 2215 @ <p>This repository does not expire email notification subscriptions.
7b8be20… drh 2216 @ No renewals are necessary.</p>
7b8be20… drh 2217 style_finish_page();
7b8be20… drh 2218 return;
7b8be20… drh 2219 }
7b8be20… drh 2220
f33976f… drh 2221 db_unprotect(PROTECT_READONLY);
7b8be20… drh 2222 db_prepare(&s,
7b8be20… drh 2223 "UPDATE subscriber"
7b8be20… drh 2224 " SET lastContact=now()/86400"
7b8be20… drh 2225 " WHERE subscriberCode=hextoblob(%Q)"
7b8be20… drh 2226 " RETURNING semail, date('now','+%d days');",
7b8be20… drh 2227 zName, iInterval+1
7b8be20… drh 2228 );
7b8be20… drh 2229 rc = db_step(&s);
7b8be20… drh 2230 if( rc==SQLITE_ROW ){
7b8be20… drh 2231 @ <p>The email notification subscription for %h(db_column_text(&s,0))
7b8be20… drh 2232 @ has been extended until %h(db_column_text(&s,1)) UTC.
7b8be20… drh 2233 }else{
7b8be20… drh 2234 @ <p>No such subscriber-id: %h(zName)</p>
7b8be20… drh 2235 }
7b8be20… drh 2236 db_finalize(&s);
f33976f… drh 2237 db_protect_pop();
7b8be20… drh 2238 style_finish_page();
7b8be20… drh 2239 }
7b8be20… drh 2240
7b8be20… drh 2241
fc5c7d2… drh 2242 /* This is the message that gets sent to describe how to change
fc5c7d2… drh 2243 ** or modify a subscription
fc5c7d2… drh 2244 */
e0576ea… stephan 2245 static const char zUnsubMsg[] =
fc5c7d2… drh 2246 @ To changes your subscription settings at %s visit this link:
fc5c7d2… drh 2247 @
fc5c7d2… drh 2248 @ %s/alerts/%s
fc5c7d2… drh 2249 @
fc5c7d2… drh 2250 @ To completely unsubscribe from %s, visit the following link:
fc5c7d2… drh 2251 @
fc5c7d2… drh 2252 @ %s/unsubscribe/%s
fc5c7d2… drh 2253 ;
fc5c7d2… drh 2254
fc5c7d2… drh 2255 /*
fc5c7d2… drh 2256 ** WEBPAGE: unsubscribe
bca95cb… drh 2257 ** WEBPAGE: oneclickunsub
fc5c7d2… drh 2258 **
fc5c7d2… drh 2259 ** Users visit this page to be delisted from email alerts.
fc5c7d2… drh 2260 **
fc5c7d2… drh 2261 ** If a valid subscriber code is supplied in the name= query parameter,
fc5c7d2… drh 2262 ** then that subscriber is delisted.
fc5c7d2… drh 2263 **
33d3bf3… km 2264 ** Otherwise, if the users are logged in, then they are redirected
fc5c7d2… drh 2265 ** to the /alerts page where they have an unsubscribe button.
fc5c7d2… drh 2266 **
fc5c7d2… drh 2267 ** Non-logged-in users with no name= query parameter are invited to enter
fc5c7d2… drh 2268 ** an email address to which will be sent the unsubscribe link that
fc5c7d2… drh 2269 ** contains the correct subscriber code.
bca95cb… drh 2270 **
bca95cb… drh 2271 ** The /unsubscribe page requires comfirmation. The /oneclickunsub
bca95cb… drh 2272 ** page unsubscribes immediately without any need to confirm.
fc5c7d2… drh 2273 */
fc5c7d2… drh 2274 void unsubscribe_page(void){
fc5c7d2… drh 2275 const char *zName = P("name");
fc5c7d2… drh 2276 char *zErr = 0;
fc5c7d2… drh 2277 int eErr = 0;
11d1233… drh 2278 unsigned int uSeed = 0;
fc5c7d2… drh 2279 const char *zDecoded;
fc5c7d2… drh 2280 char *zCaptcha = 0;
fc5c7d2… drh 2281 int dx;
fc5c7d2… drh 2282 int bSubmit;
fc5c7d2… drh 2283 const char *zEAddr;
fc5c7d2… drh 2284 char *zCode = 0;
15e1529… drh 2285 int sid = 0;
15e1529… drh 2286
f045c5d… drh 2287 if( zName==0 ) zName = P("scode");
f045c5d… drh 2288
f045c5d… drh 2289 /* If a valid subscriber code is supplied, then either present the user
e0576ea… stephan 2290 ** with a confirmation, or if already confirmed, unsubscribe immediately.
fc5c7d2… drh 2291 */
e0576ea… stephan 2292 if( zName
15e1529… drh 2293 && (sid = db_int(0, "SELECT subscriberId FROM subscriber"
15e1529… drh 2294 " WHERE subscriberCode=hextoblob(%Q)", zName))!=0
fc5c7d2… drh 2295 ){
f045c5d… drh 2296 char *zUnsubName = mprintf("confirm%04x", sid);
f045c5d… drh 2297 if( P(zUnsubName)!=0 ){
bca95cb… drh 2298 alert_unsubscribe(sid, 1);
bca95cb… drh 2299 }else if( sqlite3_strglob("*oneclick*",g.zPath)==0 ){
bca95cb… drh 2300 alert_unsubscribe(sid, 0);
f045c5d… drh 2301 }else if( P("manage")!=0 ){
f045c5d… drh 2302 cgi_redirectf("%R/alerts/%s", zName);
f045c5d… drh 2303 }else{
37f929e… drh 2304 style_header("Unsubscribe");
f045c5d… drh 2305 form_begin(0, "%R/unsubscribe");
f045c5d… drh 2306 @ <input type="hidden" name="scode" value="%h(zName)">
f045c5d… drh 2307 @ <table border="0" cellpadding="10" width="100%%">
f045c5d… drh 2308 @ <tr><td align="right">
f045c5d… drh 2309 @ <input type="submit" name="%h(zUnsubName)" value="Unsubscribe">
f045c5d… drh 2310 @ </td><td><big><b>&larr;</b></big></td>
f045c5d… drh 2311 @ <td>Cancel your subscription to %h(g.zBaseURL) notifications
f045c5d… drh 2312 @ </td><tr>
f045c5d… drh 2313 @ <tr><td align="right">
f045c5d… drh 2314 @ <input type="submit" name="manage" \
f045c5d… drh 2315 @ value="Manage Subscription Settings">
f045c5d… drh 2316 @ </td><td><big><b>&larr;</b></big></td>
37f929e… drh 2317 @ <td>Make other changes to your subscription preferences
f045c5d… drh 2318 @ </td><tr>
f045c5d… drh 2319 @ </table>
f045c5d… drh 2320 @ </form>
f045c5d… drh 2321 style_finish_page();
f045c5d… drh 2322 }
fc5c7d2… drh 2323 return;
fc5c7d2… drh 2324 }
fc5c7d2… drh 2325
fc5c7d2… drh 2326 /* Logged in users are redirected to the /alerts page */
fc5c7d2… drh 2327 login_check_credentials();
fc5c7d2… drh 2328 if( login_is_individual() ){
fc5c7d2… drh 2329 cgi_redirectf("%R/alerts");
fc5c7d2… drh 2330 return;
fc5c7d2… drh 2331 }
fc5c7d2… drh 2332
112c713… drh 2333 style_set_current_feature("alerts");
112c713… drh 2334
fc5c7d2… drh 2335 zEAddr = PD("e","");
fc5c7d2… drh 2336 dx = atoi(PD("dx","0"));
920ace1… drh 2337 bSubmit = P("submit")!=0 && P("e")!=0 && cgi_csrf_safe(2);
fc5c7d2… drh 2338 if( bSubmit ){
fc5c7d2… drh 2339 if( !captcha_is_correct(1) ){
fc5c7d2… drh 2340 eErr = 2;
fc5c7d2… drh 2341 zErr = mprintf("enter the security code shown below");
fc5c7d2… drh 2342 bSubmit = 0;
fc5c7d2… drh 2343 }
fc5c7d2… drh 2344 }
fc5c7d2… drh 2345 if( bSubmit ){
fc5c7d2… drh 2346 zCode = db_text(0,"SELECT hex(subscriberCode) FROM subscriber"
fc5c7d2… drh 2347 " WHERE semail=%Q", zEAddr);
fc5c7d2… drh 2348 if( zCode==0 ){
fc5c7d2… drh 2349 eErr = 1;
fc5c7d2… drh 2350 zErr = mprintf("not a valid email address");
fc5c7d2… drh 2351 bSubmit = 0;
fc5c7d2… drh 2352 }
fc5c7d2… drh 2353 }
fc5c7d2… drh 2354 if( bSubmit ){
fc5c7d2… drh 2355 /* If we get this far, it means that a valid unsubscribe request has
fc5c7d2… drh 2356 ** been submitted. Send the appropriate email. */
fc5c7d2… drh 2357 Blob hdr, body;
fc5c7d2… drh 2358 AlertSender *pSender = alert_sender_new(0,0);
fc5c7d2… drh 2359 blob_init(&hdr,0,0);
fc5c7d2… drh 2360 blob_init(&body,0,0);
fc5c7d2… drh 2361 blob_appendf(&hdr, "To: <%s>\r\n", zEAddr);
fc5c7d2… drh 2362 blob_appendf(&hdr, "Subject: Unsubscribe Instructions\r\n");
fc5c7d2… drh 2363 blob_appendf(&body, zUnsubMsg/*works-like:"%s%s%s%s%s%s"*/,
fc5c7d2… drh 2364 g.zBaseURL, g.zBaseURL, zCode, g.zBaseURL, g.zBaseURL, zCode);
fc5c7d2… drh 2365 alert_send(pSender, &hdr, &body, 0);
fc5c7d2… drh 2366 style_header("Unsubscribe Instructions Sent");
fc5c7d2… drh 2367 if( pSender->zErr ){
fc5c7d2… drh 2368 @ <h1>Internal Error</h1>
fc5c7d2… drh 2369 @ <p>The following error was encountered while trying to send an
fc5c7d2… drh 2370 @ email to %h(zEAddr):
fc5c7d2… drh 2371 @ <blockquote><pre>
fc5c7d2… drh 2372 @ %h(pSender->zErr)
fc5c7d2… drh 2373 @ </pre></blockquote>
fc5c7d2… drh 2374 }else{
fc5c7d2… drh 2375 @ <p>An email has been sent to "%h(zEAddr)" that explains how to
fc5c7d2… drh 2376 @ unsubscribe and/or modify your subscription settings</p>
fc5c7d2… drh 2377 }
fc5c7d2… drh 2378 alert_sender_free(pSender);
112c713… drh 2379 style_finish_page();
fc5c7d2… drh 2380 return;
e0576ea… stephan 2381 }
fc5c7d2… drh 2382
fc5c7d2… drh 2383 /* Non-logged-in users have to enter an email address to which is
fc5c7d2… drh 2384 ** sent a message containing the unsubscribe link.
fc5c7d2… drh 2385 */
fc5c7d2… drh 2386 style_header("Unsubscribe Request");
fc5c7d2… drh 2387 @ <p>Fill out the form below to request an email message that will
fc5c7d2… drh 2388 @ explain how to unsubscribe and/or change your subscription settings.</p>
fc5c7d2… drh 2389 @
fc5c7d2… drh 2390 form_begin(0, "%R/unsubscribe");
fc5c7d2… drh 2391 @ <table class="subscribe">
fc5c7d2… drh 2392 @ <tr>
fc5c7d2… drh 2393 @ <td class="form_label">Email&nbsp;Address:</td>
fc5c7d2… drh 2394 @ <td><input type="text" name="e" value="%h(zEAddr)" size="30"></td>
fc5c7d2… drh 2395 if( eErr==1 ){
fc5c7d2… drh 2396 @ <td><span class="loginError">&larr; %h(zErr)</span></td>
fc5c7d2… drh 2397 }
fc5c7d2… drh 2398 @ </tr>
fc5c7d2… drh 2399 uSeed = captcha_seed();
8659d84… drh 2400 zDecoded = captcha_decode(uSeed, 0);
fc5c7d2… drh 2401 zCaptcha = captcha_render(zDecoded);
fc5c7d2… drh 2402 @ <tr>
fc5c7d2… drh 2403 @ <td class="form_label">Security Code:</td>
fc5c7d2… drh 2404 @ <td><input type="text" name="captcha" value="" size="30">
a584491… drh 2405 captcha_speakit_button(uSeed, "Speak the code");
fc5c7d2… drh 2406 @ <input type="hidden" name="captchaseed" value="%u(uSeed)"></td>
fc5c7d2… drh 2407 if( eErr==2 ){
fc5c7d2… drh 2408 @ <td><span class="loginError">&larr; %h(zErr)</span></td>
fc5c7d2… drh 2409 }
fc5c7d2… drh 2410 @ </tr>
fc5c7d2… drh 2411 @ <tr>
fc5c7d2… drh 2412 @ <td class="form_label">Options:</td>
fc5c7d2… drh 2413 @ <td><label><input type="radio" name="dx" value="0" %s(dx?"":"checked")>\
fc5c7d2… drh 2414 @ Modify subscription</label><br>
fc5c7d2… drh 2415 @ <label><input type="radio" name="dx" value="1" %s(dx?"checked":"")>\
fc5c7d2… drh 2416 @ Completely unsubscribe</label><br>
fc5c7d2… drh 2417 @ <tr>
fc5c7d2… drh 2418 @ <td></td>
fc5c7d2… drh 2419 @ <td><input type="submit" name="submit" value="Submit"></td>
fc5c7d2… drh 2420 @ </tr>
fc5c7d2… drh 2421 @ </table>
75c89de… drh 2422 @ <div class="captcha"><table class="captcha"><tr><td><pre class="captcha">
fc5c7d2… drh 2423 @ %h(zCaptcha)
fc5c7d2… drh 2424 @ </pre>
dcf4410… drh 2425 @ Enter the 8 characters above in the "Security Code" box<br/>
fc5c7d2… drh 2426 @ </td></tr></table></div>
fc5c7d2… drh 2427 @ </form>
fc5c7d2… drh 2428 fossil_free(zErr);
112c713… drh 2429 style_finish_page();
fc5c7d2… drh 2430 }
fc5c7d2… drh 2431
fc5c7d2… drh 2432 /*
fc5c7d2… drh 2433 ** WEBPAGE: subscribers
fc5c7d2… drh 2434 **
fc5c7d2… drh 2435 ** This page, accessible to administrators only,
8c40c38… drh 2436 ** shows a list of subscriber email addresses.
fc5c7d2… drh 2437 ** Clicking on an email takes one to the /alerts page
fc5c7d2… drh 2438 ** for that email where the delivery settings can be
fc5c7d2… drh 2439 ** modified.
fc5c7d2… drh 2440 */
fc5c7d2… drh 2441 void subscriber_list_page(void){
fc5c7d2… drh 2442 Blob sql;
fc5c7d2… drh 2443 Stmt q;
fc5c7d2… drh 2444 sqlite3_int64 iNow;
8c40c38… drh 2445 int nTotal;
8c40c38… drh 2446 int nPending;
8c40c38… drh 2447 int nDel = 0;
34d45c5… drh 2448 int iCutoff = db_get_int("email-renew-cutoff",0);
34d45c5… drh 2449 int iWarning = db_get_int("email-renew-warning",0);
34d45c5… drh 2450 char zCutoffClr[8];
34d45c5… drh 2451 char zWarnClr[8];
fc5c7d2… drh 2452 if( alert_webpages_disabled() ) return;
fc5c7d2… drh 2453 login_check_credentials();
fc5c7d2… drh 2454 if( !g.perm.Admin ){
fc5c7d2… drh 2455 login_needed(0);
fc5c7d2… drh 2456 return;
fc5c7d2… drh 2457 }
fc5c7d2… drh 2458 alert_submenu_common();
54a6f09… drh 2459 style_submenu_element("Users","setup_ulist");
112c713… drh 2460 style_set_current_feature("alerts");
fc5c7d2… drh 2461 style_header("Subscriber List");
8c40c38… drh 2462 nTotal = db_int(0, "SELECT count(*) FROM subscriber");
8c40c38… drh 2463 nPending = db_int(0, "SELECT count(*) FROM subscriber WHERE NOT sverified");
8c40c38… drh 2464 if( nPending>0 && P("purge") && cgi_csrf_safe(0) ){
8c40c38… drh 2465 int nNewPending;
8c40c38… drh 2466 db_multi_exec(
8c40c38… drh 2467 "DELETE FROM subscriber"
d7e10ce… drh 2468 " WHERE NOT sverified AND mtime<now()-86400"
8c40c38… drh 2469 );
8c40c38… drh 2470 nNewPending = db_int(0, "SELECT count(*) FROM subscriber"
8c40c38… drh 2471 " WHERE NOT sverified");
8c40c38… drh 2472 nDel = nPending - nNewPending;
8c40c38… drh 2473 nPending = nNewPending;
8ccab68… drh 2474 nTotal -= nDel;
8c40c38… drh 2475 }
8c40c38… drh 2476 if( nPending>0 ){
8c40c38… drh 2477 @ <h1>%,d(nTotal) Subscribers, %,d(nPending) Pending</h1>
8c40c38… drh 2478 if( nDel==0 && 0<db_int(0,"SELECT count(*) FROM subscriber"
d7e10ce… drh 2479 " WHERE NOT sverified AND mtime<now()-86400")
8c40c38… drh 2480 ){
8c40c38… drh 2481 style_submenu_element("Purge Pending","subscribers?purge");
8c40c38… drh 2482 }
8c40c38… drh 2483 }else{
8c40c38… drh 2484 @ <h1>%,d(nTotal) Subscribers</h1>
8c40c38… drh 2485 }
8c40c38… drh 2486 if( nDel>0 ){
8c40c38… drh 2487 @ <p>*** %d(nDel) pending subscriptions deleted ***</p>
8c40c38… drh 2488 }
fc5c7d2… drh 2489 blob_init(&sql, 0, 0);
fc5c7d2… drh 2490 blob_append_sql(&sql,
15e1529… drh 2491 "SELECT subscriberId," /* 0 */
fc5c7d2… drh 2492 " semail," /* 1 */
fc5c7d2… drh 2493 " ssub," /* 2 */
fc5c7d2… drh 2494 " suname," /* 3 */
fc5c7d2… drh 2495 " sverified," /* 4 */
fc5c7d2… drh 2496 " sdigest," /* 5 */
fc5c7d2… drh 2497 " mtime," /* 6 */
54a6f09… drh 2498 " date(sctime,'unixepoch')," /* 7 */
d7e10ce… drh 2499 " (SELECT uid FROM user WHERE login=subscriber.suname)," /* 8 */
d7e10ce… drh 2500 " coalesce(lastContact,mtime/86400)" /* 9 */
fc5c7d2… drh 2501 " FROM subscriber"
fc5c7d2… drh 2502 );
fc5c7d2… drh 2503 if( P("only")!=0 ){
fc5c7d2… drh 2504 blob_append_sql(&sql, " WHERE ssub LIKE '%%%q%%'", P("only"));
fc5c7d2… drh 2505 style_submenu_element("Show All","%R/subscribers");
fc5c7d2… drh 2506 }
fc5c7d2… drh 2507 blob_append_sql(&sql," ORDER BY mtime DESC");
fc5c7d2… drh 2508 db_prepare_blob(&q, &sql);
fc5c7d2… drh 2509 iNow = time(0);
34d45c5… drh 2510 memcpy(zCutoffClr, hash_color("A"), sizeof(zCutoffClr));
34d45c5… drh 2511 memcpy(zWarnClr, hash_color("HIJ"), sizeof(zWarnClr));
fc5c7d2… drh 2512 @ <table border='1' class='sortable' \
d7e10ce… drh 2513 @ data-init-sort='6' data-column-types='tttttKKt'>
fc5c7d2… drh 2514 @ <thead>
fc5c7d2… drh 2515 @ <tr>
fc5c7d2… drh 2516 @ <th>Email
fc5c7d2… drh 2517 @ <th>Events
fc5c7d2… drh 2518 @ <th>Digest-Only?
fc5c7d2… drh 2519 @ <th>User
fc5c7d2… drh 2520 @ <th>Verified?
fc5c7d2… drh 2521 @ <th>Last change
d7e10ce… drh 2522 @ <th>Last contact
fc5c7d2… drh 2523 @ <th>Created
fc5c7d2… drh 2524 @ </tr>
fc5c7d2… drh 2525 @ </thead><tbody>
fc5c7d2… drh 2526 while( db_step(&q)==SQLITE_ROW ){
fc5c7d2… drh 2527 sqlite3_int64 iMtime = db_column_int64(&q, 6);
fc5c7d2… drh 2528 double rAge = (iNow - iMtime)/86400.0;
54a6f09… drh 2529 int uid = db_column_int(&q, 8);
54a6f09… drh 2530 const char *zUname = db_column_text(&q, 3);
d7e10ce… drh 2531 sqlite3_int64 iContact = db_column_int64(&q, 9);
57f16ce… drh 2532 double rContact = (iNow/86400.0) - iContact;
fc5c7d2… drh 2533 @ <tr>
15e1529… drh 2534 @ <td><a href='%R/alerts?sid=%d(db_column_int(&q,0))'>\
fc5c7d2… drh 2535 @ %h(db_column_text(&q,1))</a></td>
fc5c7d2… drh 2536 @ <td>%h(db_column_text(&q,2))</td>
fc5c7d2… drh 2537 @ <td>%s(db_column_int(&q,5)?"digest":"")</td>
54a6f09… drh 2538 if( uid ){
54a6f09… drh 2539 @ <td><a href='%R/setup_uedit?id=%d(uid)'>%h(zUname)</a>
54a6f09… drh 2540 }else{
54a6f09… drh 2541 @ <td>%h(zUname)</td>
54a6f09… drh 2542 }
fc5c7d2… drh 2543 @ <td>%s(db_column_int(&q,4)?"yes":"pending")</td>
fc5c7d2… drh 2544 @ <td data-sortkey='%010llx(iMtime)'>%z(human_readable_age(rAge))</td>
34d45c5… drh 2545 @ <td data-sortkey='%010llx(iContact)'>\
34d45c5… drh 2546 if( iContact>iWarning ){
34d45c5… drh 2547 @ <span>\
34d45c5… drh 2548 }else if( iContact>iCutoff ){
34d45c5… drh 2549 @ <span style='background-color:%s(zWarnClr);'>\
34d45c5… drh 2550 }else{
34d45c5… drh 2551 @ <span style='background-color:%s(zCutoffClr);'>\
34d45c5… drh 2552 }
34d45c5… drh 2553 @ %z(human_readable_age(rContact))</td>
fc5c7d2… drh 2554 @ <td>%h(db_column_text(&q,7))</td>
fc5c7d2… drh 2555 @ </tr>
fc5c7d2… drh 2556 }
fc5c7d2… drh 2557 @ </tbody></table>
fc5c7d2… drh 2558 db_finalize(&q);
fc5c7d2… drh 2559 style_table_sorter();
112c713… drh 2560 style_finish_page();
fc5c7d2… drh 2561 }
fc5c7d2… drh 2562
fc5c7d2… drh 2563 #if LOCAL_INTERFACE
fc5c7d2… drh 2564 /*
fc5c7d2… drh 2565 ** A single event that might appear in an alert is recorded as an
fc5c7d2… drh 2566 ** instance of the following object.
e5653a4… drh 2567 **
e5653a4… drh 2568 ** type values:
e5653a4… drh 2569 **
e5653a4… drh 2570 ** c A new check-in
e5653a4… drh 2571 ** f An original forum post
d4361f6… drh 2572 ** n New forum threads
d4361f6… drh 2573 ** r Replies to my forum posts
e5653a4… drh 2574 ** x An edit to a prior forum post
e5653a4… drh 2575 ** t A new ticket or a change to an existing ticket
d96055c… stephan 2576 ** u A user was added or received new permissions
e5653a4… drh 2577 ** w A change to a wiki page
d4361f6… drh 2578 ** x Edits to forum posts
fc5c7d2… drh 2579 */
fc5c7d2… drh 2580 struct EmailEvent {
d96055c… stephan 2581 int type; /* 'c', 'f', 'n', 'r', 't', 'u', 'w', 'x' */
fc5c7d2… drh 2582 int needMod; /* Pending moderator approval */
fc5c7d2… drh 2583 Blob hdr; /* Header content, for forum entries */
fc5c7d2… drh 2584 Blob txt; /* Text description to appear in an alert */
fc5c7d2… drh 2585 char *zFromName; /* Human name of the sender */
d4361f6… drh 2586 char *zPriors; /* Upthread sender IDs for forum posts */
fc5c7d2… drh 2587 EmailEvent *pNext; /* Next in chronological order */
fc5c7d2… drh 2588 };
fc5c7d2… drh 2589 #endif
fc5c7d2… drh 2590
fc5c7d2… drh 2591 /*
fc5c7d2… drh 2592 ** Free a linked list of EmailEvent objects
fc5c7d2… drh 2593 */
fc5c7d2… drh 2594 void alert_free_eventlist(EmailEvent *p){
fc5c7d2… drh 2595 while( p ){
fc5c7d2… drh 2596 EmailEvent *pNext = p->pNext;
fc5c7d2… drh 2597 blob_reset(&p->txt);
fc5c7d2… drh 2598 blob_reset(&p->hdr);
fc5c7d2… drh 2599 fossil_free(p->zFromName);
d4361f6… drh 2600 fossil_free(p->zPriors);
fc5c7d2… drh 2601 fossil_free(p);
fc5c7d2… drh 2602 p = pNext;
fc5c7d2… drh 2603 }
d4361f6… drh 2604 }
d4361f6… drh 2605
d4361f6… drh 2606 /*
d4361f6… drh 2607 ** Compute a string that is appropriate for the EmailEvent.zPriors field
d4361f6… drh 2608 ** for a particular forum post.
d4361f6… drh 2609 **
d4361f6… drh 2610 ** This string is an encode list of sender names and rids for all ancestors
33d3bf3… km 2611 ** of the fpid post - the post that fpid answers, the post that parent
d4361f6… drh 2612 ** post answers, and so forth back up to the root post. Duplicates sender
d4361f6… drh 2613 ** names are omitted.
d4361f6… drh 2614 **
d4361f6… drh 2615 ** The EmailEvent.zPriors field is used to screen events for people who
d4361f6… drh 2616 ** only want to see replies to their own posts or to specific posts.
d4361f6… drh 2617 */
d4361f6… drh 2618 static char *alert_compute_priors(int fpid){
d4361f6… drh 2619 return db_text(0,
d4361f6… drh 2620 "WITH priors(rid,who) AS ("
d4361f6… drh 2621 " SELECT firt, coalesce(euser,user)"
d4361f6… drh 2622 " FROM forumpost LEFT JOIN event ON fpid=objid"
d4361f6… drh 2623 " WHERE fpid=%d"
d4361f6… drh 2624 " UNION ALL"
d4361f6… drh 2625 " SELECT firt, coalesce(euser,user)"
d4361f6… drh 2626 " FROM priors, forumpost LEFT JOIN event ON fpid=objid"
d4361f6… drh 2627 " WHERE fpid=rid"
d4361f6… drh 2628 ")"
d4361f6… drh 2629 "SELECT ','||group_concat(DISTINCT 'u'||who)||"
d4361f6… drh 2630 "','||group_concat(rid) FROM priors;",
d4361f6… drh 2631 fpid
d4361f6… drh 2632 );
fc5c7d2… drh 2633 }
fc5c7d2… drh 2634
fc5c7d2… drh 2635 /*
fc5c7d2… drh 2636 ** Compute and return a linked list of EmailEvent objects
fc5c7d2… drh 2637 ** corresponding to the current content of the temp.wantalert
fc5c7d2… drh 2638 ** table which should be defined as follows:
fc5c7d2… drh 2639 **
fc5c7d2… drh 2640 ** CREATE TEMP TABLE wantalert(eventId TEXT, needMod BOOLEAN);
fc5c7d2… drh 2641 */
fc5c7d2… drh 2642 EmailEvent *alert_compute_event_text(int *pnEvent, int doDigest){
fc5c7d2… drh 2643 Stmt q;
fc5c7d2… drh 2644 EmailEvent *p;
fc5c7d2… drh 2645 EmailEvent anchor;
fc5c7d2… drh 2646 EmailEvent *pLast;
fc5c7d2… drh 2647 const char *zUrl = db_get("email-url","http://localhost:8080");
fc5c7d2… drh 2648 const char *zFrom;
fc5c7d2… drh 2649 const char *zSub;
fc5c7d2… drh 2650
fc5c7d2… drh 2651
fc5c7d2… drh 2652 /* First do non-forum post events */
fc5c7d2… drh 2653 db_prepare(&q,
fc5c7d2… drh 2654 "SELECT"
1a5dee1… drh 2655 " CASE WHEN event.type='t'"
1a5dee1… drh 2656 " THEN (SELECT substr(tagname,5) FROM tag"
1a5dee1… drh 2657 " WHERE tagid=event.tagid AND tagname LIKE 'tkt-%%')"
1a5dee1… drh 2658 " ELSE blob.uuid END," /* 0 */
fc5c7d2… drh 2659 " datetime(event.mtime)," /* 1 */
fc5c7d2… drh 2660 " coalesce(ecomment,comment)"
fc5c7d2… drh 2661 " || ' (user: ' || coalesce(euser,user,'?')"
fc5c7d2… drh 2662 " || (SELECT case when length(x)>0 then ' tags: ' || x else '' end"
fc5c7d2… drh 2663 " FROM (SELECT group_concat(substr(tagname,5), ', ') AS x"
fc5c7d2… drh 2664 " FROM tag, tagxref"
fc5c7d2… drh 2665 " WHERE tagname GLOB 'sym-*' AND tag.tagid=tagxref.tagid"
fc5c7d2… drh 2666 " AND tagxref.rid=blob.rid AND tagxref.tagtype>0))"
fc5c7d2… drh 2667 " || ')' as comment," /* 2 */
fc5c7d2… drh 2668 " wantalert.eventId," /* 3 */
fc5c7d2… drh 2669 " wantalert.needMod" /* 4 */
fc5c7d2… drh 2670 " FROM temp.wantalert, event, blob"
fc5c7d2… drh 2671 " WHERE blob.rid=event.objid"
fc5c7d2… drh 2672 " AND event.objid=substr(wantalert.eventId,2)+0"
fc5c7d2… drh 2673 " AND (%d OR eventId NOT GLOB 'f*')"
fc5c7d2… drh 2674 " ORDER BY event.mtime",
fc5c7d2… drh 2675 doDigest
fc5c7d2… drh 2676 );
fc5c7d2… drh 2677 memset(&anchor, 0, sizeof(anchor));
fc5c7d2… drh 2678 pLast = &anchor;
fc5c7d2… drh 2679 *pnEvent = 0;
fc5c7d2… drh 2680 while( db_step(&q)==SQLITE_ROW ){
fc5c7d2… drh 2681 const char *zType = "";
7fcfa93… stephan 2682 const char *zComment = db_column_text(&q, 2);
33877fa… drh 2683 p = fossil_malloc_zero( sizeof(EmailEvent) );
fc5c7d2… drh 2684 pLast->pNext = p;
fc5c7d2… drh 2685 pLast = p;
fc5c7d2… drh 2686 p->type = db_column_text(&q, 3)[0];
fc5c7d2… drh 2687 p->needMod = db_column_int(&q, 4);
fc5c7d2… drh 2688 p->zFromName = 0;
fc5c7d2… drh 2689 p->pNext = 0;
fc5c7d2… drh 2690 switch( p->type ){
fc5c7d2… drh 2691 case 'c': zType = "Check-In"; break;
e5653a4… drh 2692 /* case 'f': -- forum posts omitted from this loop. See below */
a8856c6… drh 2693 case 't': zType = "Ticket Change"; break;
7fcfa93… stephan 2694 case 'w': {
7fcfa93… stephan 2695 zType = "Wiki Edit";
7fcfa93… stephan 2696 switch( zComment ? *zComment : 0 ){
7fcfa93… stephan 2697 case ':': ++zComment; break;
7fcfa93… stephan 2698 case '+': zType = "Wiki Added"; ++zComment; break;
7fcfa93… stephan 2699 case '-': zType = "Wiki Removed"; ++zComment; break;
7fcfa93… stephan 2700 }
7fcfa93… stephan 2701 break;
7fcfa93… stephan 2702 }
fc5c7d2… drh 2703 }
fc5c7d2… drh 2704 blob_init(&p->hdr, 0, 0);
fc5c7d2… drh 2705 blob_init(&p->txt, 0, 0);
fc5c7d2… drh 2706 blob_appendf(&p->txt,"== %s %s ==\n%s\n%s/info/%.20s\n",
fc5c7d2… drh 2707 db_column_text(&q,1),
fc5c7d2… drh 2708 zType,
7fcfa93… stephan 2709 zComment,
fc5c7d2… drh 2710 zUrl,
fc5c7d2… drh 2711 db_column_text(&q,0)
fc5c7d2… drh 2712 );
fc5c7d2… drh 2713 if( p->needMod ){
fc5c7d2… drh 2714 blob_appendf(&p->txt,
fc5c7d2… drh 2715 "** Pending moderator approval (%s/modreq) **\n",
fc5c7d2… drh 2716 zUrl
fc5c7d2… drh 2717 );
fc5c7d2… drh 2718 }
fc5c7d2… drh 2719 (*pnEvent)++;
fc5c7d2… drh 2720 }
fc5c7d2… drh 2721 db_finalize(&q);
fc5c7d2… drh 2722
fc5c7d2… drh 2723 /* Early-out if forumpost is not a table in this repository */
fc5c7d2… drh 2724 if( !db_table_exists("repository","forumpost") ){
fc5c7d2… drh 2725 return anchor.pNext;
fc5c7d2… drh 2726 }
fc5c7d2… drh 2727
fc5c7d2… drh 2728 /* For digests, the previous loop also handled forumposts already */
fc5c7d2… drh 2729 if( doDigest ){
fc5c7d2… drh 2730 return anchor.pNext;
fc5c7d2… drh 2731 }
fc5c7d2… drh 2732
fc5c7d2… drh 2733 /* If we reach this point, it means that forumposts exist and this
fc5c7d2… drh 2734 ** is a normal email alert. Construct full-text forum post alerts
fc5c7d2… drh 2735 ** using a format that enables them to be sent as separate emails.
fc5c7d2… drh 2736 */
fc5c7d2… drh 2737 db_prepare(&q,
fc5c7d2… drh 2738 "SELECT"
fb4545e… drh 2739 " forumpost.fpid," /* 0: fpid */
fb4545e… drh 2740 " (SELECT uuid FROM blob WHERE rid=forumpost.fpid)," /* 1: hash */
fb4545e… drh 2741 " datetime(event.mtime)," /* 2: date/time */
fb4545e… drh 2742 " substr(comment,instr(comment,':')+2)," /* 3: comment */
fb4545e… drh 2743 " (WITH thread(fpid,fprev) AS ("
fb4545e… drh 2744 " SELECT fpid,fprev FROM forumpost AS tx"
fb4545e… drh 2745 " WHERE tx.froot=forumpost.froot),"
fb4545e… drh 2746 " basepid(fpid,bpid) AS ("
fb4545e… drh 2747 " SELECT fpid, fpid FROM thread WHERE fprev IS NULL"
fb4545e… drh 2748 " UNION ALL"
fb4545e… drh 2749 " SELECT thread.fpid, basepid.bpid FROM basepid, thread"
fb4545e… drh 2750 " WHERE basepid.fpid=thread.fprev)"
fb4545e… drh 2751 " SELECT uuid FROM blob, basepid"
fb4545e… drh 2752 " WHERE basepid.fpid=forumpost.firt"
fb4545e… drh 2753 " AND blob.rid=basepid.bpid)," /* 4: in-reply-to */
fb4545e… drh 2754 " wantalert.needMod," /* 5: moderated */
fb4545e… drh 2755 " coalesce(display_name(info),euser,user)," /* 6: user */
fb4545e… drh 2756 " forumpost.fprev IS NULL" /* 7: is an edit */
fc5c7d2… drh 2757 " FROM temp.wantalert, event, forumpost"
fc5c7d2… drh 2758 " LEFT JOIN user ON (login=coalesce(euser,user))"
fc5c7d2… drh 2759 " WHERE event.objid=substr(wantalert.eventId,2)+0"
fc5c7d2… drh 2760 " AND eventId GLOB 'f*'"
fc5c7d2… drh 2761 " AND forumpost.fpid=event.objid"
fc5c7d2… drh 2762 " ORDER BY event.mtime"
fc5c7d2… drh 2763 );
fc5c7d2… drh 2764 zFrom = db_get("email-self",0);
fc5c7d2… drh 2765 zSub = db_get("email-subname","");
fc5c7d2… drh 2766 while( db_step(&q)==SQLITE_ROW ){
d4361f6… drh 2767 int fpid = db_column_int(&q,0);
d4361f6… drh 2768 Manifest *pPost = manifest_get(fpid, CFTYPE_FORUM, 0);
fc5c7d2… drh 2769 const char *zIrt;
fc5c7d2… drh 2770 const char *zUuid;
fc5c7d2… drh 2771 const char *zTitle;
fc5c7d2… drh 2772 const char *z;
fc5c7d2… drh 2773 if( pPost==0 ) continue;
fc5c7d2… drh 2774 p = fossil_malloc( sizeof(EmailEvent) );
fc5c7d2… drh 2775 pLast->pNext = p;
fc5c7d2… drh 2776 pLast = p;
e5653a4… drh 2777 p->type = db_column_int(&q,7) ? 'f' : 'x';
fc5c7d2… drh 2778 p->needMod = db_column_int(&q, 5);
fc5c7d2… drh 2779 z = db_column_text(&q,6);
fc5c7d2… drh 2780 p->zFromName = z && z[0] ? fossil_strdup(z) : 0;
d4361f6… drh 2781 p->zPriors = alert_compute_priors(fpid);
fc5c7d2… drh 2782 p->pNext = 0;
fc5c7d2… drh 2783 blob_init(&p->hdr, 0, 0);
fc5c7d2… drh 2784 zUuid = db_column_text(&q, 1);
fc5c7d2… drh 2785 zTitle = db_column_text(&q, 3);
fc5c7d2… drh 2786 if( p->needMod ){
fc5c7d2… drh 2787 blob_appendf(&p->hdr, "Subject: %s Pending Moderation: %s\r\n",
fc5c7d2… drh 2788 zSub, zTitle);
fc5c7d2… drh 2789 }else{
fc5c7d2… drh 2790 blob_appendf(&p->hdr, "Subject: %s %s\r\n", zSub, zTitle);
e0576ea… stephan 2791 blob_appendf(&p->hdr, "Message-Id: <%.32s@%s>\r\n",
fc5c7d2… drh 2792 zUuid, alert_hostname(zFrom));
fc5c7d2… drh 2793 zIrt = db_column_text(&q, 4);
fc5c7d2… drh 2794 if( zIrt && zIrt[0] ){
fc5c7d2… drh 2795 blob_appendf(&p->hdr, "In-Reply-To: <%.32s@%s>\r\n",
fc5c7d2… drh 2796 zIrt, alert_hostname(zFrom));
fc5c7d2… drh 2797 }
fc5c7d2… drh 2798 }
fc5c7d2… drh 2799 blob_init(&p->txt, 0, 0);
fc5c7d2… drh 2800 if( p->needMod ){
fc5c7d2… drh 2801 blob_appendf(&p->txt,
fc5c7d2… drh 2802 "** Pending moderator approval (%s/modreq) **\n",
fc5c7d2… drh 2803 zUrl
fc5c7d2… drh 2804 );
fc5c7d2… drh 2805 }
fc5c7d2… drh 2806 blob_appendf(&p->txt,
fc5c7d2… drh 2807 "Forum post by %s on %s\n",
fc5c7d2… drh 2808 pPost->zUser, db_column_text(&q, 2));
fc5c7d2… drh 2809 blob_appendf(&p->txt, "%s/forumpost/%S\n\n", zUrl, zUuid);
fc5c7d2… drh 2810 blob_append(&p->txt, pPost->zWiki, -1);
fc5c7d2… drh 2811 manifest_destroy(pPost);
fc5c7d2… drh 2812 (*pnEvent)++;
fc5c7d2… drh 2813 }
fc5c7d2… drh 2814 db_finalize(&q);
fc5c7d2… drh 2815
fc5c7d2… drh 2816 return anchor.pNext;
fc5c7d2… drh 2817 }
fc5c7d2… drh 2818
fc5c7d2… drh 2819 /*
fc5c7d2… drh 2820 ** Put a header on an alert email
fc5c7d2… drh 2821 */
fc5c7d2… drh 2822 void email_header(Blob *pOut){
fc5c7d2… drh 2823 blob_appendf(pOut,
fc5c7d2… drh 2824 "This is an automated email reporting changes "
fc5c7d2… drh 2825 "on Fossil repository %s (%s/timeline)\n",
fc5c7d2… drh 2826 db_get("email-subname","(unknown)"),
fc5c7d2… drh 2827 db_get("email-url","http://localhost:8080"));
fc5c7d2… drh 2828 }
fc5c7d2… drh 2829
fc5c7d2… drh 2830 /*
fc5c7d2… drh 2831 ** COMMAND: test-alert
fc5c7d2… drh 2832 **
fc5c7d2… drh 2833 ** Usage: %fossil test-alert EVENTID ...
fc5c7d2… drh 2834 **
fc5c7d2… drh 2835 ** Generate the text of an email alert for all of the EVENTIDs
fc5c7d2… drh 2836 ** listed on the command-line. Or if no events are listed on the
fc5c7d2… drh 2837 ** command line, generate text for all events named in the
34d45c5… drh 2838 ** pending_alert table. The text of the email alerts appears on
34d45c5… drh 2839 ** standard output.
34d45c5… drh 2840 **
34d45c5… drh 2841 ** This command is intended for testing and debugging Fossil itself,
34d45c5… drh 2842 ** for example when enhancing the email alert system or fixing bugs
34d45c5… drh 2843 ** in the email alert system. If you are not making changes to the
34d45c5… drh 2844 ** Fossil source code, this command is probably not useful to you.
fc5c7d2… drh 2845 **
34d45c5… drh 2846 ** EVENTIDs are text. The first character is 'c', 'f', 't', or 'w'
34d45c5… drh 2847 ** for check-in, forum, ticket, or wiki. The remaining text is a
34d45c5… drh 2848 ** integer that references the EVENT.OBJID value for the event.
34d45c5… drh 2849 ** Run /timeline?showid to see these OBJID values.
fc5c7d2… drh 2850 **
fc5c7d2… drh 2851 ** Options:
fc5c7d2… drh 2852 ** --digest Generate digest alert text
fc5c7d2… drh 2853 ** --needmod Assume all events are pending moderator approval
fc5c7d2… drh 2854 */
fc5c7d2… drh 2855 void test_alert_cmd(void){
fc5c7d2… drh 2856 Blob out;
fc5c7d2… drh 2857 int nEvent;
fc5c7d2… drh 2858 int needMod;
fc5c7d2… drh 2859 int doDigest;
fc5c7d2… drh 2860 EmailEvent *pEvent, *p;
fc5c7d2… drh 2861
fc5c7d2… drh 2862 doDigest = find_option("digest",0,0)!=0;
fc5c7d2… drh 2863 needMod = find_option("needmod",0,0)!=0;
fc5c7d2… drh 2864 db_find_and_open_repository(0, 0);
fc5c7d2… drh 2865 verify_all_options();
fc5c7d2… drh 2866 db_begin_transaction();
fc5c7d2… drh 2867 alert_schema(0);
fc5c7d2… drh 2868 db_multi_exec("CREATE TEMP TABLE wantalert(eventid TEXT, needMod BOOLEAN)");
fc5c7d2… drh 2869 if( g.argc==2 ){
fc5c7d2… drh 2870 db_multi_exec(
fc5c7d2… drh 2871 "INSERT INTO wantalert(eventId,needMod)"
fc5c7d2… drh 2872 " SELECT eventid, %d FROM pending_alert", needMod);
fc5c7d2… drh 2873 }else{
fc5c7d2… drh 2874 int i;
fc5c7d2… drh 2875 for(i=2; i<g.argc; i++){
fc5c7d2… drh 2876 db_multi_exec("INSERT INTO wantalert(eventId,needMod) VALUES(%Q,%d)",
fc5c7d2… drh 2877 g.argv[i], needMod);
fc5c7d2… drh 2878 }
fc5c7d2… drh 2879 }
fc5c7d2… drh 2880 blob_init(&out, 0, 0);
fc5c7d2… drh 2881 email_header(&out);
fc5c7d2… drh 2882 pEvent = alert_compute_event_text(&nEvent, doDigest);
fc5c7d2… drh 2883 for(p=pEvent; p; p=p->pNext){
fc5c7d2… drh 2884 blob_append(&out, "\n", 1);
fc5c7d2… drh 2885 if( blob_size(&p->hdr) ){
fc5c7d2… drh 2886 blob_append(&out, blob_buffer(&p->hdr), blob_size(&p->hdr));
fc5c7d2… drh 2887 blob_append(&out, "\n", 1);
fc5c7d2… drh 2888 }
fc5c7d2… drh 2889 blob_append(&out, blob_buffer(&p->txt), blob_size(&p->txt));
fc5c7d2… drh 2890 }
fc5c7d2… drh 2891 alert_free_eventlist(pEvent);
fc5c7d2… drh 2892 fossil_print("%s", blob_str(&out));
fc5c7d2… drh 2893 blob_reset(&out);
fc5c7d2… drh 2894 db_end_transaction(0);
fc5c7d2… drh 2895 }
fc5c7d2… drh 2896
fc5c7d2… drh 2897 /*
fc5c7d2… drh 2898 ** COMMAND: test-add-alerts
fc5c7d2… drh 2899 **
fc5c7d2… drh 2900 ** Usage: %fossil test-add-alerts [OPTIONS] EVENTID ...
fc5c7d2… drh 2901 **
fc5c7d2… drh 2902 ** Add one or more events to the pending_alert queue. Use this
fc5c7d2… drh 2903 ** command during testing to force email notifications for specific
fc5c7d2… drh 2904 ** events.
fc5c7d2… drh 2905 **
fc5c7d2… drh 2906 ** EVENTIDs are text. The first character is 'c', 'f', 't', or 'w'
fc5c7d2… drh 2907 ** for check-in, forum, ticket, or wiki. The remaining text is a
fc5c7d2… drh 2908 ** integer that references the EVENT.OBJID value for the event.
fc5c7d2… drh 2909 ** Run /timeline?showid to see these OBJID values.
fc5c7d2… drh 2910 **
fc5c7d2… drh 2911 ** Options:
fc5c7d2… drh 2912 ** --backoffice Run alert_backoffice() after all alerts have
fc5c7d2… drh 2913 ** been added. This will cause the alerts to be
fc5c7d2… drh 2914 ** sent out with the SENDALERT_TRACE option.
fc5c7d2… drh 2915 ** --debug Like --backoffice, but add the SENDALERT_STDOUT
fc5c7d2… drh 2916 ** so that emails are printed to standard output
fc5c7d2… drh 2917 ** rather than being sent.
fc5c7d2… drh 2918 ** --digest Process emails using SENDALERT_DIGEST
fc5c7d2… drh 2919 */
fc5c7d2… drh 2920 void test_add_alert_cmd(void){
fc5c7d2… drh 2921 int i;
fc5c7d2… drh 2922 int doAuto = find_option("backoffice",0,0)!=0;
fc5c7d2… drh 2923 unsigned mFlags = 0;
fc5c7d2… drh 2924 if( find_option("debug",0,0)!=0 ){
fc5c7d2… drh 2925 doAuto = 1;
fc5c7d2… drh 2926 mFlags = SENDALERT_STDOUT;
fc5c7d2… drh 2927 }
fc5c7d2… drh 2928 if( find_option("digest",0,0)!=0 ){
fc5c7d2… drh 2929 mFlags |= SENDALERT_DIGEST;
fc5c7d2… drh 2930 }
fc5c7d2… drh 2931 db_find_and_open_repository(0, 0);
fc5c7d2… drh 2932 verify_all_options();
fc5c7d2… drh 2933 db_begin_write();
fc5c7d2… drh 2934 alert_schema(0);
fc5c7d2… drh 2935 for(i=2; i<g.argc; i++){
fc5c7d2… drh 2936 db_multi_exec("REPLACE INTO pending_alert(eventId) VALUES(%Q)", g.argv[i]);
fc5c7d2… drh 2937 }
fc5c7d2… drh 2938 db_end_transaction(0);
fc5c7d2… drh 2939 if( doAuto ){
fc5c7d2… drh 2940 alert_backoffice(SENDALERT_TRACE|mFlags);
fc5c7d2… drh 2941 }
fc5c7d2… drh 2942 }
fc5c7d2… drh 2943
34d45c5… drh 2944 /*
1508169… drh 2945 ** Minimum number of days between renewal messages
1508169… drh 2946 */
1508169… drh 2947 #define ALERT_RENEWAL_MSG_FREQUENCY 7 /* Do renewals at most once/week */
1508169… drh 2948
1508169… drh 2949 /*
34d45c5… drh 2950 ** Construct the header and body for an email message that will alert
34d45c5… drh 2951 ** a subscriber that their subscriptions are about to expire.
34d45c5… drh 2952 */
34d45c5… drh 2953 static void alert_renewal_msg(
34d45c5… drh 2954 Blob *pHdr, /* Write email header here */
34d45c5… drh 2955 Blob *pBody, /* Write email body here */
34d45c5… drh 2956 const char *zCode, /* The subscriber code */
34d45c5… drh 2957 int lastContact, /* Last contact (days since 1970) */
34d45c5… drh 2958 const char *zEAddr, /* Subscriber email address. Send to this. */
34d45c5… drh 2959 const char *zSub, /* Subscription codes */
e2bdc10… danield 2960 const char *zRepoName, /* Name of the sending Fossil repository */
e2bdc10… danield 2961 const char *zUrl /* URL for the sending Fossil repository */
34d45c5… drh 2962 ){
34d45c5… drh 2963 blob_appendf(pHdr,"To: <%s>\r\n", zEAddr);
34d45c5… drh 2964 blob_appendf(pHdr,"Subject: %s Subscription to %s expires soon\r\n",
34d45c5… drh 2965 zRepoName, zUrl);
34d45c5… drh 2966 blob_appendf(pBody,
1508169… drh 2967 "\nTo renew your subscription, click the following link:\n"
1508169… drh 2968 "\n %s/renew/%s\n\n",
1508169… drh 2969 zUrl, zCode
1508169… drh 2970 );
1508169… drh 2971 blob_appendf(pBody,
1508169… drh 2972 "You are currently receiving email notification for the following events\n"
1508169… drh 2973 "on the %s Fossil repository at %s:\n\n",
34d45c5… drh 2974 zRepoName, zUrl
34d45c5… drh 2975 );
34d45c5… drh 2976 if( strchr(zSub, 'a') ) blob_appendf(pBody, " * Announcements\n");
34d45c5… drh 2977 if( strchr(zSub, 'c') ) blob_appendf(pBody, " * Check-ins\n");
34d45c5… drh 2978 if( strchr(zSub, 'f') ) blob_appendf(pBody, " * Forum posts\n");
34d45c5… drh 2979 if( strchr(zSub, 't') ) blob_appendf(pBody, " * Ticket changes\n");
d96055c… stephan 2980 if( strchr(zSub, 'u') ) blob_appendf(pBody, " * User permission elevation\n");
34d45c5… drh 2981 if( strchr(zSub, 'w') ) blob_appendf(pBody, " * Wiki changes\n");
1508169… drh 2982 blob_appendf(pBody, "\n"
34d45c5… drh 2983 "If you take no action, your subscription will expire and you will be\n"
1508169… drh 2984 "unsubscribed in about %d days. To make other changes or to unsubscribe\n"
34d45c5… drh 2985 "immediately, visit the following webpage:\n\n"
34d45c5… drh 2986 " %s/alerts/%s\n\n",
1508169… drh 2987 ALERT_RENEWAL_MSG_FREQUENCY, zUrl, zCode
34d45c5… drh 2988 );
d4361f6… drh 2989 }
d4361f6… drh 2990
d4361f6… drh 2991 /*
d4361f6… drh 2992 ** If zUser is a sender of one of the ancestors of a forum post
d4361f6… drh 2993 ** (if zUser appears in zPriors) then return true.
d4361f6… drh 2994 */
d4361f6… drh 2995 static int alert_in_priors(const char *zUser, const char *zPriors){
d4361f6… drh 2996 int n = (int)strlen(zUser);
d4361f6… drh 2997 char zBuf[200];
d4361f6… drh 2998 if( n>195 ) return 0;
d4361f6… drh 2999 if( zPriors==0 || zPriors[0]==0 ) return 0;
d4361f6… drh 3000 zBuf[0] = ',';
d4361f6… drh 3001 zBuf[1] = 'u';
d4361f6… drh 3002 memcpy(zBuf+2, zUser, n+1);
d4361f6… drh 3003 return strstr(zPriors, zBuf)!=0;
34d45c5… drh 3004 }
34d45c5… drh 3005
fc5c7d2… drh 3006 #if INTERFACE
fc5c7d2… drh 3007 /*
fc5c7d2… drh 3008 ** Flags for alert_send_alerts()
fc5c7d2… drh 3009 */
fc5c7d2… drh 3010 #define SENDALERT_DIGEST 0x0001 /* Send a digest */
fc5c7d2… drh 3011 #define SENDALERT_PRESERVE 0x0002 /* Do not mark the task as done */
fc5c7d2… drh 3012 #define SENDALERT_STDOUT 0x0004 /* Print emails instead of sending */
fc5c7d2… drh 3013 #define SENDALERT_TRACE 0x0008 /* Trace operation for debugging */
34d45c5… drh 3014 #define SENDALERT_RENEWAL 0x0010 /* Send renewal notices */
fc5c7d2… drh 3015
fc5c7d2… drh 3016 #endif /* INTERFACE */
fc5c7d2… drh 3017
fc5c7d2… drh 3018 /*
fc5c7d2… drh 3019 ** Send alert emails to subscribers.
fc5c7d2… drh 3020 **
fc5c7d2… drh 3021 ** This procedure is run by either the backoffice, or in response to the
fc5c7d2… drh 3022 ** "fossil alerts send" command. Details of operation are controlled by
fc5c7d2… drh 3023 ** the flags parameter.
fc5c7d2… drh 3024 **
fc5c7d2… drh 3025 ** Here is a summary of what happens:
fc5c7d2… drh 3026 **
fc5c7d2… drh 3027 ** (1) Create a TEMP table wantalert(eventId,needMod) and fill it with
fc5c7d2… drh 3028 ** all the events that we want to send alerts about. The needMod
fc5c7d2… drh 3029 ** flags is set if and only if the event is still awaiting
fc5c7d2… drh 3030 ** moderator approval. Events with the needMod flag are only
fc5c7d2… drh 3031 ** shown to users that have moderator privileges.
fc5c7d2… drh 3032 **
fc5c7d2… drh 3033 ** (2) Call alert_compute_event_text() to compute a list of EmailEvent
fc5c7d2… drh 3034 ** objects that describe all events about which we want to send
fc5c7d2… drh 3035 ** alerts.
fc5c7d2… drh 3036 **
fc5c7d2… drh 3037 ** (3) Loop over all subscribers. Compose and send one or more email
fc5c7d2… drh 3038 ** messages to each subscriber that describe the events for
fc5c7d2… drh 3039 ** which the subscriber has expressed interest and has
fc5c7d2… drh 3040 ** appropriate privileges.
fc5c7d2… drh 3041 **
fc5c7d2… drh 3042 ** (4) Update the pending_alerts table to indicate that alerts have been
fc5c7d2… drh 3043 ** sent.
fc5c7d2… drh 3044 **
fc5c7d2… drh 3045 ** Update 2018-08-09: Do step (3) before step (4). Update the
fc5c7d2… drh 3046 ** pending_alerts table *before* the emails are sent. That way, if
fc5c7d2… drh 3047 ** the process malfunctions or crashes, some notifications may never
fc5c7d2… drh 3048 ** be sent. But that is better than some recurring bug causing
fc5c7d2… drh 3049 ** subscribers to be flooded with repeated notifications every 60
fc5c7d2… drh 3050 ** seconds!
fc5c7d2… drh 3051 */
71b9f35… drh 3052 int alert_send_alerts(u32 flags){
fc5c7d2… drh 3053 EmailEvent *pEvents, *p;
fc5c7d2… drh 3054 int nEvent = 0;
71b9f35… drh 3055 int nSent = 0;
fc5c7d2… drh 3056 Stmt q;
fc5c7d2… drh 3057 const char *zDigest = "false";
fc5c7d2… drh 3058 Blob hdr, body;
fc5c7d2… drh 3059 const char *zUrl;
fc5c7d2… drh 3060 const char *zRepoName;
fc5c7d2… drh 3061 const char *zFrom;
fc5c7d2… drh 3062 const char *zDest = (flags & SENDALERT_STDOUT) ? "stdout" : 0;
fc5c7d2… drh 3063 AlertSender *pSender = 0;
fc5c7d2… drh 3064 u32 senderFlags = 0;
34d45c5… drh 3065 int iInterval = 0; /* Subscription renewal interval */
fc5c7d2… drh 3066
fc5c7d2… drh 3067 if( g.fSqlTrace ) fossil_trace("-- BEGIN alert_send_alerts(%u)\n", flags);
fc5c7d2… drh 3068 alert_schema(0);
4a3909a… drh 3069 if( !alert_enabled() && (flags & SENDALERT_STDOUT)==0 ) goto send_alert_done;
fc5c7d2… drh 3070 zUrl = db_get("email-url",0);
fc5c7d2… drh 3071 if( zUrl==0 ) goto send_alert_done;
fc5c7d2… drh 3072 zRepoName = db_get("email-subname",0);
fc5c7d2… drh 3073 if( zRepoName==0 ) goto send_alert_done;
fc5c7d2… drh 3074 zFrom = db_get("email-self",0);
fc5c7d2… drh 3075 if( zFrom==0 ) goto send_alert_done;
fc5c7d2… drh 3076 if( flags & SENDALERT_TRACE ){
fc5c7d2… drh 3077 senderFlags |= ALERT_TRACE;
fc5c7d2… drh 3078 }
fc5c7d2… drh 3079 pSender = alert_sender_new(zDest, senderFlags);
fc5c7d2… drh 3080
fc5c7d2… drh 3081 /* Step (1): Compute the alerts that need sending
fc5c7d2… drh 3082 */
fc5c7d2… drh 3083 db_multi_exec(
fc5c7d2… drh 3084 "DROP TABLE IF EXISTS temp.wantalert;"
fc5c7d2… drh 3085 "CREATE TEMP TABLE wantalert(eventId TEXT, needMod BOOLEAN, sentMod);"
fc5c7d2… drh 3086 );
fc5c7d2… drh 3087 if( flags & SENDALERT_DIGEST ){
fc5c7d2… drh 3088 /* Unmoderated changes are never sent as part of a digest */
fc5c7d2… drh 3089 db_multi_exec(
fc5c7d2… drh 3090 "INSERT INTO wantalert(eventId,needMod)"
fc5c7d2… drh 3091 " SELECT eventid, 0"
fc5c7d2… drh 3092 " FROM pending_alert"
fc5c7d2… drh 3093 " WHERE sentDigest IS FALSE"
fc5c7d2… drh 3094 " AND NOT EXISTS(SELECT 1 FROM private WHERE rid=substr(eventid,2));"
fc5c7d2… drh 3095 );
fc5c7d2… drh 3096 zDigest = "true";
fc5c7d2… drh 3097 }else{
fc5c7d2… drh 3098 /* Immediate alerts might include events that are subject to
fc5c7d2… drh 3099 ** moderator approval */
fc5c7d2… drh 3100 db_multi_exec(
fc5c7d2… drh 3101 "INSERT INTO wantalert(eventId,needMod,sentMod)"
fc5c7d2… drh 3102 " SELECT eventid,"
fc5c7d2… drh 3103 " EXISTS(SELECT 1 FROM private WHERE rid=substr(eventid,2)),"
fc5c7d2… drh 3104 " sentMod"
fc5c7d2… drh 3105 " FROM pending_alert"
fc5c7d2… drh 3106 " WHERE sentSep IS FALSE;"
fc5c7d2… drh 3107 "DELETE FROM wantalert WHERE needMod AND sentMod;"
fc5c7d2… drh 3108 );
fc5c7d2… drh 3109 }
fb040bb… drh 3110 if( g.fSqlTrace ){
fb040bb… drh 3111 fossil_trace("-- wantalert contains %d rows\n",
fb040bb… drh 3112 db_int(0, "SELECT count(*) FROM wantalert")
fb040bb… drh 3113 );
fb040bb… drh 3114 }
fc5c7d2… drh 3115
fc5c7d2… drh 3116 /* Step 2: compute EmailEvent objects for every notification that
fc5c7d2… drh 3117 ** needs sending.
fc5c7d2… drh 3118 */
fc5c7d2… drh 3119 pEvents = alert_compute_event_text(&nEvent, (flags & SENDALERT_DIGEST)!=0);
34d45c5… drh 3120 if( nEvent==0 ) goto send_alert_expiration_warnings;
fc5c7d2… drh 3121
fc5c7d2… drh 3122 /* Step 4a: Update the pending_alerts table to designate the
fc5c7d2… drh 3123 ** alerts as having all been sent. This is done *before* step (3)
fc5c7d2… drh 3124 ** so that a crash will not cause alerts to be sent multiple times.
fc5c7d2… drh 3125 ** Better a missed alert than being spammed with hundreds of alerts
fc5c7d2… drh 3126 ** due to a bug.
fc5c7d2… drh 3127 */
fc5c7d2… drh 3128 if( (flags & SENDALERT_PRESERVE)==0 ){
fc5c7d2… drh 3129 if( flags & SENDALERT_DIGEST ){
fc5c7d2… drh 3130 db_multi_exec(
fc5c7d2… drh 3131 "UPDATE pending_alert SET sentDigest=true"
fc5c7d2… drh 3132 " WHERE eventid IN (SELECT eventid FROM wantalert);"
fc5c7d2… drh 3133 );
fc5c7d2… drh 3134 }else{
fc5c7d2… drh 3135 db_multi_exec(
fc5c7d2… drh 3136 "UPDATE pending_alert SET sentSep=true"
fc5c7d2… drh 3137 " WHERE eventid IN (SELECT eventid FROM wantalert WHERE NOT needMod);"
fc5c7d2… drh 3138 "UPDATE pending_alert SET sentMod=true"
fc5c7d2… drh 3139 " WHERE eventid IN (SELECT eventid FROM wantalert WHERE needMod);"
fc5c7d2… drh 3140 );
fc5c7d2… drh 3141 }
fc5c7d2… drh 3142 }
fc5c7d2… drh 3143
fc5c7d2… drh 3144 /* Step 3: Loop over subscribers. Send alerts
fc5c7d2… drh 3145 */
fc5c7d2… drh 3146 blob_init(&hdr, 0, 0);
fc5c7d2… drh 3147 blob_init(&body, 0, 0);
fc5c7d2… drh 3148 db_prepare(&q,
fc5c7d2… drh 3149 "SELECT"
fc5c7d2… drh 3150 " hex(subscriberCode)," /* 0 */
fc5c7d2… drh 3151 " semail," /* 1 */
fc5c7d2… drh 3152 " ssub," /* 2 */
d4361f6… drh 3153 " fullcap(user.cap)," /* 3 */
d4361f6… drh 3154 " suname" /* 4 */
fc5c7d2… drh 3155 " FROM subscriber LEFT JOIN user ON (login=suname)"
34d45c5… drh 3156 " WHERE sverified"
34d45c5… drh 3157 " AND NOT sdonotcall"
34d45c5… drh 3158 " AND sdigest IS %s"
2917bed… drh 3159 " AND coalesce(subscriber.lastContact*86400,subscriber.mtime)>=%d",
34d45c5… drh 3160 zDigest/*safe-for-%s*/,
34d45c5… drh 3161 db_get_int("email-renew-cutoff",0)
fc5c7d2… drh 3162 );
fc5c7d2… drh 3163 while( db_step(&q)==SQLITE_ROW ){
fc5c7d2… drh 3164 const char *zCode = db_column_text(&q, 0);
fc5c7d2… drh 3165 const char *zSub = db_column_text(&q, 2);
fc5c7d2… drh 3166 const char *zEmail = db_column_text(&q, 1);
fc5c7d2… drh 3167 const char *zCap = db_column_text(&q, 3);
2917bed… drh 3168 const char *zUser = db_column_text(&q, 4);
fc5c7d2… drh 3169 int nHit = 0;
fc5c7d2… drh 3170 for(p=pEvents; p; p=p->pNext){
d4361f6… drh 3171 if( strchr(zSub,p->type)==0 ){
d4361f6… drh 3172 if( p->type!='f' ) continue;
d4361f6… drh 3173 if( strchr(zSub,'n')!=0 && (p->zPriors==0 || p->zPriors[0]==0) ){
d4361f6… drh 3174 /* New post: accepted */
2917bed… drh 3175 }else if( strchr(zSub,'r')!=0 && zUser!=0
2917bed… drh 3176 && alert_in_priors(zUser, p->zPriors) ){
d4361f6… drh 3177 /* A follow-up to a post written by the user: accept */
d4361f6… drh 3178 }else{
d4361f6… drh 3179 continue;
d4361f6… drh 3180 }
d4361f6… drh 3181 }
fc5c7d2… drh 3182 if( p->needMod ){
fc5c7d2… drh 3183 /* For events that require moderator approval, only send an alert
1d3dd1d… wyoung 3184 ** if the recipient is a moderator for that type of event. Setup
1d3dd1d… wyoung 3185 ** and Admin users always get notified. */
fc5c7d2… drh 3186 char xType = '*';
1d3dd1d… wyoung 3187 if( strpbrk(zCap,"as")==0 ){
1d3dd1d… wyoung 3188 switch( p->type ){
d4361f6… drh 3189 case 'x': case 'f':
d4361f6… drh 3190 case 'n': case 'r': xType = '5'; break;
e5653a4… drh 3191 case 't': xType = 'q'; break;
e5653a4… drh 3192 case 'w': xType = 'l'; break;
d96055c… stephan 3193 /* Note: case 'u' is not handled here */
1d3dd1d… wyoung 3194 }
1d3dd1d… wyoung 3195 if( strchr(zCap,xType)==0 ) continue;
fc5c7d2… drh 3196 }
fc5c7d2… drh 3197 }else if( strchr(zCap,'s')!=0 || strchr(zCap,'a')!=0 ){
fc5c7d2… drh 3198 /* Setup and admin users can get any notification that does not
fc5c7d2… drh 3199 ** require moderation */
fc5c7d2… drh 3200 }else{
fc5c7d2… drh 3201 /* Other users only see the alert if they have sufficient
fc5c7d2… drh 3202 ** privilege to view the event itself */
fc5c7d2… drh 3203 char xType = '*';
fc5c7d2… drh 3204 switch( p->type ){
e5653a4… drh 3205 case 'c': xType = 'o'; break;
d4361f6… drh 3206 case 'x': case 'f':
d4361f6… drh 3207 case 'n': case 'r': xType = '2'; break;
e5653a4… drh 3208 case 't': xType = 'r'; break;
e5653a4… drh 3209 case 'w': xType = 'j'; break;
d96055c… stephan 3210 /* Note: case 'u' is not handled here */
fc5c7d2… drh 3211 }
fc5c7d2… drh 3212 if( strchr(zCap,xType)==0 ) continue;
fc5c7d2… drh 3213 }
fc5c7d2… drh 3214 if( blob_size(&p->hdr)>0 ){
fc5c7d2… drh 3215 /* This alert should be sent as a separate email */
fc5c7d2… drh 3216 Blob fhdr, fbody;
fc5c7d2… drh 3217 blob_init(&fhdr, 0, 0);
fc5c7d2… drh 3218 blob_appendf(&fhdr, "To: <%s>\r\n", zEmail);
fc5c7d2… drh 3219 blob_append(&fhdr, blob_buffer(&p->hdr), blob_size(&p->hdr));
fc5c7d2… drh 3220 blob_init(&fbody, blob_buffer(&p->txt), blob_size(&p->txt));
908612e… drh 3221 if( pSender->zListId && pSender->zListId[0] ){
908612e… drh 3222 blob_appendf(&fhdr, "List-Id: %s\r\n", pSender->zListId);
908612e… drh 3223 blob_appendf(&fhdr, "List-Unsubscribe: <%s/oneclickunsub/%s>\r\n",
908612e… drh 3224 zUrl, zCode);
908612e… drh 3225 blob_appendf(&fhdr,
908612e… drh 3226 "List-Unsubscribe-Post: List-Unsubscribe=One-Click\r\n");
908612e… drh 3227 blob_appendf(&fbody, "\n-- \nUnsubscribe: %s/unsubscribe/%s\n",
908612e… drh 3228 zUrl, zCode);
908612e… drh 3229 /* blob_appendf(&fbody, "Subscription settings: %s/alerts/%s\n",
908612e… drh 3230 ** zUrl, zCode); */
908612e… drh 3231 }
fc5c7d2… drh 3232 alert_send(pSender,&fhdr,&fbody,p->zFromName);
71b9f35… drh 3233 nSent++;
fc5c7d2… drh 3234 blob_reset(&fhdr);
fc5c7d2… drh 3235 blob_reset(&fbody);
fc5c7d2… drh 3236 }else{
fc5c7d2… drh 3237 /* Events other than forum posts are gathered together into
fc5c7d2… drh 3238 ** a single email message */
fc5c7d2… drh 3239 if( nHit==0 ){
fc5c7d2… drh 3240 blob_appendf(&hdr,"To: <%s>\r\n", zEmail);
fc5c7d2… drh 3241 blob_appendf(&hdr,"Subject: %s activity alert\r\n", zRepoName);
fc5c7d2… drh 3242 blob_appendf(&body,
fc5c7d2… drh 3243 "This is an automated email sent by the Fossil repository "
fc5c7d2… drh 3244 "at %s to report changes.\n",
fc5c7d2… drh 3245 zUrl
fc5c7d2… drh 3246 );
fc5c7d2… drh 3247 }
fc5c7d2… drh 3248 nHit++;
fc5c7d2… drh 3249 blob_append(&body, "\n", 1);
fc5c7d2… drh 3250 blob_append(&body, blob_buffer(&p->txt), blob_size(&p->txt));
fc5c7d2… drh 3251 }
fc5c7d2… drh 3252 }
fc5c7d2… drh 3253 if( nHit==0 ) continue;
908612e… drh 3254 if( pSender->zListId && pSender->zListId[0] ){
908612e… drh 3255 blob_appendf(&hdr, "List-Id: %s\r\n", pSender->zListId);
908612e… drh 3256 blob_appendf(&hdr, "List-Unsubscribe: <%s/oneclickunsub/%s>\r\n",
908612e… drh 3257 zUrl, zCode);
908612e… drh 3258 blob_appendf(&hdr,
908612e… drh 3259 "List-Unsubscribe-Post: List-Unsubscribe=One-Click\r\n");
908612e… drh 3260 blob_appendf(&body,"\n-- \nSubscription info: %s/alerts/%s\n",
908612e… drh 3261 zUrl, zCode);
908612e… drh 3262 }
fc5c7d2… drh 3263 alert_send(pSender,&hdr,&body,0);
71b9f35… drh 3264 nSent++;
fc5c7d2… drh 3265 blob_truncate(&hdr, 0);
fc5c7d2… drh 3266 blob_truncate(&body, 0);
fc5c7d2… drh 3267 }
fc5c7d2… drh 3268 blob_reset(&hdr);
fc5c7d2… drh 3269 blob_reset(&body);
fc5c7d2… drh 3270 db_finalize(&q);
fc5c7d2… drh 3271 alert_free_eventlist(pEvents);
fc5c7d2… drh 3272
fc5c7d2… drh 3273 /* Step 4b: Update the pending_alerts table to remove all of the
fc5c7d2… drh 3274 ** alerts that have been completely sent.
fc5c7d2… drh 3275 */
fc5c7d2… drh 3276 db_multi_exec("DELETE FROM pending_alert WHERE sentDigest AND sentSep;");
34d45c5… drh 3277
34d45c5… drh 3278 /* Send renewal messages to subscribers whose subscriptions are about
34d45c5… drh 3279 ** to expire. Only do this if:
34d45c5… drh 3280 **
275da70… danield 3281 ** (1) email-renew-interval is 14 or greater (or in other words if
34d45c5… drh 3282 ** subscription expiration is enabled).
34d45c5… drh 3283 **
34d45c5… drh 3284 ** (2) The SENDALERT_RENEWAL flag is set
34d45c5… drh 3285 */
34d45c5… drh 3286 send_alert_expiration_warnings:
34d45c5… drh 3287 if( (flags & SENDALERT_RENEWAL)!=0
34d45c5… drh 3288 && (iInterval = db_get_int("email-renew-interval",0))>=14
34d45c5… drh 3289 ){
34d45c5… drh 3290 int iNow = (int)(time(0)/86400);
34d45c5… drh 3291 int iOldWarn = db_get_int("email-renew-warning",0);
34d45c5… drh 3292 int iNewWarn = iNow - iInterval + ALERT_RENEWAL_MSG_FREQUENCY;
34d45c5… drh 3293 if( iNewWarn >= iOldWarn + ALERT_RENEWAL_MSG_FREQUENCY ){
34d45c5… drh 3294 db_prepare(&q,
34d45c5… drh 3295 "SELECT"
34d45c5… drh 3296 " hex(subscriberCode)," /* 0 */
34d45c5… drh 3297 " lastContact," /* 1 */
34d45c5… drh 3298 " semail," /* 2 */
34d45c5… drh 3299 " ssub" /* 3 */
34d45c5… drh 3300 " FROM subscriber"
34d45c5… drh 3301 " WHERE lastContact<=%d AND lastContact>%d"
34d45c5… drh 3302 " AND NOT sdonotcall"
34d45c5… drh 3303 " AND length(sdigest)>0",
34d45c5… drh 3304 iNewWarn, iOldWarn
34d45c5… drh 3305 );
34d45c5… drh 3306 while( db_step(&q)==SQLITE_ROW ){
34d45c5… drh 3307 Blob hdr, body;
908612e… drh 3308 const char *zCode = db_column_text(&q,0);
34d45c5… drh 3309 blob_init(&hdr, 0, 0);
34d45c5… drh 3310 blob_init(&body, 0, 0);
e0576ea… stephan 3311 alert_renewal_msg(&hdr, &body,
908612e… drh 3312 zCode,
34d45c5… drh 3313 db_column_int(&q,1),
34d45c5… drh 3314 db_column_text(&q,2),
34d45c5… drh 3315 db_column_text(&q,3),
34d45c5… drh 3316 zRepoName, zUrl);
908612e… drh 3317 if( pSender->zListId && pSender->zListId[0] ){
908612e… drh 3318 blob_appendf(&hdr, "List-Id: %s\r\n", pSender->zListId);
908612e… drh 3319 blob_appendf(&hdr, "List-Unsubscribe: <%s/oneclickunsub/%s>\r\n",
908612e… drh 3320 zUrl, zCode);
908612e… drh 3321 blob_appendf(&hdr,
908612e… drh 3322 "List-Unsubscribe-Post: List-Unsubscribe=One-Click\r\n");
908612e… drh 3323 blob_appendf(&body, "\n-- \nUnsubscribe: %s/unsubscribe/%s\n",
908612e… drh 3324 zUrl, zCode);
908612e… drh 3325 }
34d45c5… drh 3326 alert_send(pSender,&hdr,&body,0);
34d45c5… drh 3327 blob_reset(&hdr);
34d45c5… drh 3328 blob_reset(&body);
34d45c5… drh 3329 }
34d45c5… drh 3330 db_finalize(&q);
34d45c5… drh 3331 if( (flags & SENDALERT_PRESERVE)==0 ){
34d45c5… drh 3332 if( iOldWarn>0 ){
34d45c5… drh 3333 db_set_int("email-renew-cutoff", iOldWarn, 0);
34d45c5… drh 3334 }
34d45c5… drh 3335 db_set_int("email-renew-warning", iNewWarn, 0);
34d45c5… drh 3336 }
34d45c5… drh 3337 }
34d45c5… drh 3338 }
71b9f35… drh 3339
fc5c7d2… drh 3340 send_alert_done:
fc5c7d2… drh 3341 alert_sender_free(pSender);
fc5c7d2… drh 3342 if( g.fSqlTrace ) fossil_trace("-- END alert_send_alerts(%u)\n", flags);
71b9f35… drh 3343 return nSent;
fc5c7d2… drh 3344 }
fc5c7d2… drh 3345
fc5c7d2… drh 3346 /*
fc5c7d2… drh 3347 ** Do backoffice processing for email notifications. In other words,
fc5c7d2… drh 3348 ** check to see if any email notifications need to occur, and then
fc5c7d2… drh 3349 ** do them.
fc5c7d2… drh 3350 **
fc5c7d2… drh 3351 ** This routine is intended to run in the background, after webpages.
fc5c7d2… drh 3352 **
fc5c7d2… drh 3353 ** The mFlags option is zero or more of the SENDALERT_* flags. Normally
fc5c7d2… drh 3354 ** this flag is zero, but the test-set-alert command sets it to
fc5c7d2… drh 3355 ** SENDALERT_TRACE.
fc5c7d2… drh 3356 */
71b9f35… drh 3357 int alert_backoffice(u32 mFlags){
fc5c7d2… drh 3358 int iJulianDay;
71b9f35… drh 3359 int nSent = 0;
71b9f35… drh 3360 if( !alert_tables_exist() ) return 0;
77377e6… drh 3361 nSent = alert_send_alerts(mFlags);
fc5c7d2… drh 3362 iJulianDay = db_int(0, "SELECT julianday('now')");
fc5c7d2… drh 3363 if( iJulianDay>db_get_int("email-last-digest",0) ){
fc5c7d2… drh 3364 db_set_int("email-last-digest",iJulianDay,0);
34d45c5… drh 3365 nSent += alert_send_alerts(SENDALERT_DIGEST|SENDALERT_RENEWAL|mFlags);
fc5c7d2… drh 3366 }
71b9f35… drh 3367 return nSent;
fc5c7d2… drh 3368 }
fc5c7d2… drh 3369
fc5c7d2… drh 3370 /*
fc5c7d2… drh 3371 ** WEBPAGE: contact_admin
fc5c7d2… drh 3372 **
fc5c7d2… drh 3373 ** A web-form to send an email message to the repository administrator,
fc5c7d2… drh 3374 ** or (with appropriate permissions) to anybody.
fc5c7d2… drh 3375 */
fc5c7d2… drh 3376 void contact_admin_page(void){
fc5c7d2… drh 3377 const char *zAdminEmail = db_get("email-admin",0);
fc5c7d2… drh 3378 unsigned int uSeed = 0;
fc5c7d2… drh 3379 const char *zDecoded;
fc5c7d2… drh 3380 char *zCaptcha = 0;
fc5c7d2… drh 3381
fc5c7d2… drh 3382 login_check_credentials();
112c713… drh 3383 style_set_current_feature("alerts");
fc5c7d2… drh 3384 if( zAdminEmail==0 || zAdminEmail[0]==0 ){
fc5c7d2… drh 3385 style_header("Outbound Email Disabled");
fc5c7d2… drh 3386 @ <p>Outbound email is disabled on this repository
112c713… drh 3387 style_finish_page();
fc5c7d2… drh 3388 return;
fc5c7d2… drh 3389 }
e0576ea… stephan 3390 if( P("submit")!=0
fc5c7d2… drh 3391 && P("subject")!=0
fc5c7d2… drh 3392 && P("msg")!=0
fc5c7d2… drh 3393 && P("from")!=0
920ace1… drh 3394 && cgi_csrf_safe(2)
fc5c7d2… drh 3395 && captcha_is_correct(0)
fc5c7d2… drh 3396 ){
fc5c7d2… drh 3397 Blob hdr, body;
fc5c7d2… drh 3398 AlertSender *pSender = alert_sender_new(0,0);
fc5c7d2… drh 3399 blob_init(&hdr, 0, 0);
fc5c7d2… drh 3400 blob_appendf(&hdr, "To: <%s>\r\nSubject: %s administrator message\r\n",
fc5c7d2… drh 3401 zAdminEmail, db_get("email-subname","Fossil Repo"));
fc5c7d2… drh 3402 blob_init(&body, 0, 0);
fc5c7d2… drh 3403 blob_appendf(&body, "Message from [%s]\n", PT("from")/*safe-for-%s*/);
fc5c7d2… drh 3404 blob_appendf(&body, "Subject: [%s]\n\n", PT("subject")/*safe-for-%s*/);
fc5c7d2… drh 3405 blob_appendf(&body, "%s", PT("msg")/*safe-for-%s*/);
fc5c7d2… drh 3406 alert_send(pSender, &hdr, &body, 0);
fc5c7d2… drh 3407 style_header("Message Sent");
fc5c7d2… drh 3408 if( pSender->zErr ){
fc5c7d2… drh 3409 @ <h1>Internal Error</h1>
fc5c7d2… drh 3410 @ <p>The following error was reported by the system:
fc5c7d2… drh 3411 @ <blockquote><pre>
fc5c7d2… drh 3412 @ %h(pSender->zErr)
fc5c7d2… drh 3413 @ </pre></blockquote>
fc5c7d2… drh 3414 }else{
fc5c7d2… drh 3415 @ <p>Your message has been sent to the repository administrator.
fc5c7d2… drh 3416 @ Thank you for your input.</p>
fc5c7d2… drh 3417 }
fc5c7d2… drh 3418 alert_sender_free(pSender);
112c713… drh 3419 style_finish_page();
fc5c7d2… drh 3420 return;
fc5c7d2… drh 3421 }
fc5c7d2… drh 3422 if( captcha_needed() ){
fc5c7d2… drh 3423 uSeed = captcha_seed();
8659d84… drh 3424 zDecoded = captcha_decode(uSeed, 0);
fc5c7d2… drh 3425 zCaptcha = captcha_render(zDecoded);
fc5c7d2… drh 3426 }
112c713… drh 3427 style_set_current_feature("alerts");
fc5c7d2… drh 3428 style_header("Message To Administrator");
fc5c7d2… drh 3429 form_begin(0, "%R/contact_admin");
fc5c7d2… drh 3430 @ <p>Enter a message to the repository administrator below:</p>
fc5c7d2… drh 3431 @ <table class="subscribe">
fc5c7d2… drh 3432 if( zCaptcha ){
fc5c7d2… drh 3433 @ <tr>
fc5c7d2… drh 3434 @ <td class="form_label">Security&nbsp;Code:</td>
fc5c7d2… drh 3435 @ <td><input type="text" name="captcha" value="" size="10">
a584491… drh 3436 captcha_speakit_button(uSeed, "Speak the code");
fc5c7d2… drh 3437 @ <input type="hidden" name="captchaseed" value="%u(uSeed)"></td>
fc5c7d2… drh 3438 @ </tr>
fc5c7d2… drh 3439 }
fc5c7d2… drh 3440 @ <tr>
fc5c7d2… drh 3441 @ <td class="form_label">Your&nbsp;Email&nbsp;Address:</td>
fc5c7d2… drh 3442 @ <td><input type="text" name="from" value="%h(PT("from"))" size="30"></td>
fc5c7d2… drh 3443 @ </tr>
fc5c7d2… drh 3444 @ <tr>
fc5c7d2… drh 3445 @ <td class="form_label">Subject:</td>
fc5c7d2… drh 3446 @ <td><input type="text" name="subject" value="%h(PT("subject"))"\
fc5c7d2… drh 3447 @ size="80"></td>
fc5c7d2… drh 3448 @ </tr>
fc5c7d2… drh 3449 @ <tr>
fc5c7d2… drh 3450 @ <td class="form_label">Message:</td>
fc5c7d2… drh 3451 @ <td><textarea name="msg" cols="80" rows="10" wrap="virtual">\
fc5c7d2… drh 3452 @ %h(PT("msg"))</textarea>
fc5c7d2… drh 3453 @ </tr>
fc5c7d2… drh 3454 @ <tr>
fc5c7d2… drh 3455 @ <td></td>
fc5c7d2… drh 3456 @ <td><input type="submit" name="submit" value="Send Message">
fc5c7d2… drh 3457 @ </tr>
fc5c7d2… drh 3458 @ </table>
fc5c7d2… drh 3459 if( zCaptcha ){
75c89de… drh 3460 @ <div class="captcha"><table class="captcha"><tr><td><pre class="captcha">
fc5c7d2… drh 3461 @ %h(zCaptcha)
fc5c7d2… drh 3462 @ </pre>
dcf4410… drh 3463 @ Enter the 8 characters above in the "Security Code" box<br/>
fc5c7d2… drh 3464 @ </td></tr></table></div>
fc5c7d2… drh 3465 }
fc5c7d2… drh 3466 @ </form>
112c713… drh 3467 style_finish_page();
fc5c7d2… drh 3468 }
fc5c7d2… drh 3469
fc5c7d2… drh 3470 /*
e2bdc10… danield 3471 ** Send an announcement message described by query parameter.
fc5c7d2… drh 3472 ** Permission to do this has already been verified.
fc5c7d2… drh 3473 */
fc5c7d2… drh 3474 static char *alert_send_announcement(void){
fc5c7d2… drh 3475 AlertSender *pSender;
fc5c7d2… drh 3476 char *zErr;
fc5c7d2… drh 3477 const char *zTo = PT("to");
fc5c7d2… drh 3478 char *zSubject = PT("subject");
fc5c7d2… drh 3479 int bAll = PB("all");
fc5c7d2… drh 3480 int bAA = PB("aa");
84d854c… drh 3481 int bMods = PB("mods");
fc5c7d2… drh 3482 const char *zSub = db_get("email-subname", "[Fossil Repo]");
bbfca4c… drh 3483 const char *zName = P("name"); /* Debugging options */
bbfca4c… drh 3484 const char *zDest = 0; /* How to send the announcement */
bbfca4c… drh 3485 int bTest = 0;
fc5c7d2… drh 3486 Blob hdr, body;
bbfca4c… drh 3487
bbfca4c… drh 3488 if( fossil_strcmp(zName, "test2")==0 ){
bbfca4c… drh 3489 bTest = 2;
bbfca4c… drh 3490 zDest = "blob";
bbfca4c… drh 3491 }else if( fossil_strcmp(zName, "test3")==0 ){
bbfca4c… drh 3492 bTest = 3;
bbfca4c… drh 3493 if( fossil_strcmp(db_get("email-send-method",""),"relay")==0 ){
bbfca4c… drh 3494 zDest = "debug-relay";
bbfca4c… drh 3495 }
bbfca4c… drh 3496 }
fc5c7d2… drh 3497 blob_init(&body, 0, 0);
fc5c7d2… drh 3498 blob_init(&hdr, 0, 0);
fc5c7d2… drh 3499 blob_appendf(&body, "%s", PT("msg")/*safe-for-%s*/);
bbfca4c… drh 3500 pSender = alert_sender_new(zDest, 0);
fc5c7d2… drh 3501 if( zTo[0] ){
fc5c7d2… drh 3502 blob_appendf(&hdr, "To: <%s>\r\nSubject: %s %s\r\n", zTo, zSub, zSubject);
fc5c7d2… drh 3503 alert_send(pSender, &hdr, &body, 0);
fc5c7d2… drh 3504 }
84d854c… drh 3505 if( bAll || bAA || bMods ){
fc5c7d2… drh 3506 Stmt q;
fc5c7d2… drh 3507 int nUsed = blob_size(&body);
fc5c7d2… drh 3508 const char *zURL = db_get("email-url",0);
84d854c… drh 3509 if( bAll ){
84d854c… drh 3510 db_prepare(&q, "SELECT semail, hex(subscriberCode) FROM subscriber "
84d854c… drh 3511 " WHERE sverified AND NOT sdonotcall");
84d854c… drh 3512 }else if( bAA ){
84d854c… drh 3513 db_prepare(&q, "SELECT semail, hex(subscriberCode) FROM subscriber "
84d854c… drh 3514 " WHERE sverified AND NOT sdonotcall"
84d854c… drh 3515 " AND ssub LIKE '%%a%%'");
84d854c… drh 3516 }else if( bMods ){
84d854c… drh 3517 db_prepare(&q,
84d854c… drh 3518 "SELECT semail, hex(subscriberCode)"
84d854c… drh 3519 " FROM subscriber, user "
84d854c… drh 3520 " WHERE sverified AND NOT sdonotcall"
84d854c… drh 3521 " AND suname=login"
84d854c… drh 3522 " AND fullcap(cap) GLOB '*5*'");
84d854c… drh 3523 }
fc5c7d2… drh 3524 while( db_step(&q)==SQLITE_ROW ){
fc5c7d2… drh 3525 const char *zCode = db_column_text(&q, 1);
fc5c7d2… drh 3526 zTo = db_column_text(&q, 0);
fc5c7d2… drh 3527 blob_truncate(&hdr, 0);
fc5c7d2… drh 3528 blob_appendf(&hdr, "To: <%s>\r\nSubject: %s %s\r\n", zTo, zSub, zSubject);
fc5c7d2… drh 3529 if( zURL ){
fc5c7d2… drh 3530 blob_truncate(&body, nUsed);
fc5c7d2… drh 3531 blob_appendf(&body,"\n-- \nSubscription info: %s/alerts/%s\n",
fc5c7d2… drh 3532 zURL, zCode);
fc5c7d2… drh 3533 }
fc5c7d2… drh 3534 alert_send(pSender, &hdr, &body, 0);
fc5c7d2… drh 3535 }
fc5c7d2… drh 3536 db_finalize(&q);
fc5c7d2… drh 3537 }
056c83d… drh 3538 if( bTest && blob_size(&pSender->out) ){
056c83d… drh 3539 /* If the URL is "/announce/test2" then no email is actually sent.
056c83d… drh 3540 ** Instead, the text of the email that would have been sent is
056c83d… drh 3541 ** displayed in the result window.
056c83d… drh 3542 **
056c83d… drh 3543 ** If the URL is "/announce/test3" and the email-send-method is "relay"
056c83d… drh 3544 ** then the announcement is sent as it normally would be, but a
056c83d… drh 3545 ** transcript of the SMTP conversation with the MTA is shown here.
056c83d… drh 3546 */
056c83d… drh 3547 blob_trim(&pSender->out);
bbfca4c… drh 3548 @ <pre style='border: 2px solid blue; padding: 1ex;'>
fc5c7d2… drh 3549 @ %h(blob_str(&pSender->out))
fc5c7d2… drh 3550 @ </pre>
bbfca4c… drh 3551 blob_reset(&pSender->out);
fc5c7d2… drh 3552 }
fc5c7d2… drh 3553 zErr = pSender->zErr;
fc5c7d2… drh 3554 pSender->zErr = 0;
fc5c7d2… drh 3555 alert_sender_free(pSender);
fc5c7d2… drh 3556 return zErr;
fc5c7d2… drh 3557 }
fc5c7d2… drh 3558
fc5c7d2… drh 3559
fc5c7d2… drh 3560 /*
fc5c7d2… drh 3561 ** WEBPAGE: announce
fc5c7d2… drh 3562 **
fc5c7d2… drh 3563 ** A web-form, available to users with the "Send-Announcement" or "A"
fc5c7d2… drh 3564 ** capability, that allows one to send announcements to whomever
fc5c7d2… drh 3565 ** has subscribed to receive announcements. The administrator can
fc5c7d2… drh 3566 ** also send a message to an arbitrary email address and/or to all
fc5c7d2… drh 3567 ** subscribers regardless of whether or not they have elected to
fc5c7d2… drh 3568 ** receive announcements.
fc5c7d2… drh 3569 */
fc5c7d2… drh 3570 void announce_page(void){
bbfca4c… drh 3571 const char *zAction = "announce";
bbfca4c… drh 3572 const char *zName = PD("name","");
bbfca4c… drh 3573 /*
bbfca4c… drh 3574 ** Debugging Notes:
bbfca4c… drh 3575 **
bbfca4c… drh 3576 ** /announce/test1 -> Shows query parameter values
bbfca4c… drh 3577 ** /announce/test2 -> Shows the formatted message but does
bbfca4c… drh 3578 ** not send it.
bbfca4c… drh 3579 ** /announce/test3 -> Sends the message, but also shows
bbfca4c… drh 3580 ** the SMTP transcript.
bbfca4c… drh 3581 */
fc5c7d2… drh 3582 login_check_credentials();
fc5c7d2… drh 3583 if( !g.perm.Announce ){
fc5c7d2… drh 3584 login_needed(0);
fc5c7d2… drh 3585 return;
fc5c7d2… drh 3586 }
bbfca4c… drh 3587 if( !g.perm.Setup ){
bbfca4c… drh 3588 zName = 0; /* Disable debugging feature for non-admin users */
bbfca4c… drh 3589 }
112c713… drh 3590 style_set_current_feature("alerts");
bbfca4c… drh 3591 if( fossil_strcmp(zName,"test1")==0 ){
fc5c7d2… drh 3592 /* Visit the /announce/test1 page to see the CGI variables */
2fa43ef… stephan 3593 zAction = "announce/test1";
fc5c7d2… drh 3594 @ <p style='border: 1px solid black; padding: 1ex;'>
0204f4a… drh 3595 cgi_print_all(0, 0, 0);
fc5c7d2… drh 3596 @ </p>
920ace1… drh 3597 }else if( P("submit")!=0 && cgi_csrf_safe(2) ){
fc5c7d2… drh 3598 char *zErr = alert_send_announcement();
fc5c7d2… drh 3599 style_header("Announcement Sent");
fc5c7d2… drh 3600 if( zErr ){
056c83d… drh 3601 @ <h1>Error</h1>
056c83d… drh 3602 @ <p>The following error was reported by the
056c83d… drh 3603 @ announcement-sending subsystem:
fc5c7d2… drh 3604 @ <blockquote><pre>
fc5c7d2… drh 3605 @ %h(zErr)
fc5c7d2… drh 3606 @ </pre></blockquote>
fc5c7d2… drh 3607 }else{
84d854c… drh 3608 @ <p>The announcement has been sent.
84d854c… drh 3609 @ <a href="%h(PD("REQUEST_URI","/"))">Send another</a></p>
fc5c7d2… drh 3610 }
112c713… drh 3611 style_finish_page();
ea64695… wyoung 3612 return;
ea64695… wyoung 3613 } else if( !alert_enabled() ){
ea64695… wyoung 3614 style_header("Cannot Send Announcement");
ea64695… wyoung 3615 @ <p>Either you have no subscribers yet, or email alerts are not yet
ea64695… wyoung 3616 @ <a href="https://fossil-scm.org/fossil/doc/trunk/www/alerts.md">set up</a>
ea64695… wyoung 3617 @ for this repository.</p>
fc5c7d2… drh 3618 return;
fc5c7d2… drh 3619 }
ea64695… wyoung 3620
fc5c7d2… drh 3621 style_header("Send Announcement");
4859a91… drh 3622 alert_submenu_common();
bbfca4c… drh 3623 if( fossil_strcmp(zName,"test2")==0 ){
bbfca4c… drh 3624 zAction = "announce/test2";
bbfca4c… drh 3625 }else if( fossil_strcmp(zName,"test3")==0 ){
bbfca4c… drh 3626 zAction = "announce/test3";
bbfca4c… drh 3627 }
2fa43ef… stephan 3628 @ <form method="POST" action="%R/%s(zAction)">
920ace1… drh 3629 login_insert_csrf_secret();
fc5c7d2… drh 3630 @ <table class="subscribe">
fc5c7d2… drh 3631 if( g.perm.Admin ){
fc5c7d2… drh 3632 int aa = PB("aa");
fc5c7d2… drh 3633 int all = PB("all");
84d854c… drh 3634 int aMod = PB("mods");
fc5c7d2… drh 3635 const char *aack = aa ? "checked" : "";
fc5c7d2… drh 3636 const char *allck = all ? "checked" : "";
84d854c… drh 3637 const char *modck = aMod ? "checked" : "";
fc5c7d2… drh 3638 @ <tr>
fc5c7d2… drh 3639 @ <td class="form_label">To:</td>
fc5c7d2… drh 3640 @ <td><input type="text" name="to" value="%h(PT("to"))" size="30"><br>
fc5c7d2… drh 3641 @ <label><input type="checkbox" name="aa" %s(aack)> \
fc5c7d2… drh 3642 @ All "announcement" subscribers</label> \
fc5c7d2… drh 3643 @ <a href="%R/subscribers?only=a" target="_blank">(list)</a><br>
fc5c7d2… drh 3644 @ <label><input type="checkbox" name="all" %s(allck)> \
fc5c7d2… drh 3645 @ All subscribers</label> \
84d854c… drh 3646 @ <a href="%R/subscribers" target="_blank">(list)</a><br>
84d854c… drh 3647 @ <label><input type="checkbox" name="mods" %s(modck)> \
84d854c… drh 3648 @ All moderators</label> \
84d854c… drh 3649 @ <a href="%R/setup_ulist?with=5" target="_blank">(list)</a><br></td>
fc5c7d2… drh 3650 @ </tr>
fc5c7d2… drh 3651 }
fc5c7d2… drh 3652 @ <tr>
fc5c7d2… drh 3653 @ <td class="form_label">Subject:</td>
fc5c7d2… drh 3654 @ <td><input type="text" name="subject" value="%h(PT("subject"))"\
fc5c7d2… drh 3655 @ size="80"></td>
fc5c7d2… drh 3656 @ </tr>
fc5c7d2… drh 3657 @ <tr>
fc5c7d2… drh 3658 @ <td class="form_label">Message:</td>
fc5c7d2… drh 3659 @ <td><textarea name="msg" cols="80" rows="10" wrap="virtual">\
fc5c7d2… drh 3660 @ %h(PT("msg"))</textarea>
fc5c7d2… drh 3661 @ </tr>
fc5c7d2… drh 3662 @ <tr>
fc5c7d2… drh 3663 @ <td></td>
bbfca4c… drh 3664 if( fossil_strcmp(zName,"test2")==0 ){
84d854c… drh 3665 @ <td><input type="submit" name="submit" value="Dry Run">
84d854c… drh 3666 }else{
84d854c… drh 3667 @ <td><input type="submit" name="submit" value="Send Message">
84d854c… drh 3668 }
fc5c7d2… drh 3669 @ </tr>
fc5c7d2… drh 3670 @ </table>
fc5c7d2… drh 3671 @ </form>
c98eec2… drh 3672 if( g.perm.Setup ){
c98eec2… drh 3673 @ <hr>
c98eec2… drh 3674 @ <p>Trouble-shooting Options:</p>
c98eec2… drh 3675 @ <ol>
c98eec2… drh 3676 @ <li> <a href="%R/announce">Normal Processing</a>
c98eec2… drh 3677 @ <li> Only <a href="%R/announce/test1">show POST parameters</a>
c98eec2… drh 3678 @ - Do not send the announcement.
c98eec2… drh 3679 @ <li> <a href="%R/announce/test2">Show the email text</a> but do
c98eec2… drh 3680 @ not actually send it.
c98eec2… drh 3681 @ <li> Send the message and also <a href="%R/announce/test3">show the
c98eec2… drh 3682 @ SMTP traffic</a> when using "relay" mode.
c98eec2… drh 3683 @ </ol>
c98eec2… drh 3684 }
112c713… drh 3685 style_finish_page();
fc5c7d2… drh 3686 }

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button