| | @@ -19,10 +19,15 @@ |
| 19 | 19 | */ |
| 20 | 20 | #include "config.h" |
| 21 | 21 | #include "email.h" |
| 22 | 22 | #include <assert.h> |
| 23 | 23 | |
| 24 | +/* |
| 25 | +** Maximum size of the subscriberCode blob, in bytes |
| 26 | +*/ |
| 27 | +#define SUBSCRIBER_CODE_SZ 32 |
| 28 | + |
| 24 | 29 | /* |
| 25 | 30 | ** SQL code to implement the tables needed by the email notification |
| 26 | 31 | ** system. |
| 27 | 32 | */ |
| 28 | 33 | static const char zEmailInit[] = |
| | @@ -42,34 +47,34 @@ |
| 42 | 47 | @ -- we might also add a separate table that allows subscribing to email |
| 43 | 48 | @ -- notifications for specific branches or tags or tickets. |
| 44 | 49 | @ -- |
| 45 | 50 | @ CREATE TABLE repository.subscriber( |
| 46 | 51 | @ subscriberId INTEGER PRIMARY KEY, -- numeric subscriber ID. Internal use |
| 47 | | -@ subscriberCode TEXT UNIQUE, -- UUID for subscriber. External use |
| 48 | | -@ sname TEXT, -- Human readable name |
| 49 | | -@ suname TEXT, -- Corresponding USER or NULL |
| 50 | | -@ semail TEXT, -- email address |
| 51 | | -@ sverify BOOLEAN, -- email address verified |
| 52 | +@ subscriberCode BLOB UNIQUE, -- UUID for subscriber. External use |
| 53 | +@ semail TEXT UNIQUE COLLATE nocase,-- email address |
| 54 | +@ suname TEXT, -- corresponding USER entry |
| 55 | +@ sverified BOOLEAN, -- email address verified |
| 52 | 56 | @ sdonotcall BOOLEAN, -- true for Do Not Call |
| 53 | 57 | @ sdigest BOOLEAN, -- true for daily digests only |
| 54 | 58 | @ ssub TEXT, -- baseline subscriptions |
| 55 | 59 | @ sctime DATE, -- When this entry was created. JulianDay |
| 56 | 60 | @ smtime DATE, -- Last change. JulianDay |
| 57 | | -@ sipaddr TEXT, -- IP address for last change |
| 58 | | -@ spswdHash TEXT -- SHA3 hash of password |
| 61 | +@ smip TEXT -- IP address of last change |
| 59 | 62 | @ ); |
| 63 | +@ CREATE INDEX repository.subscriberUname |
| 64 | +@ ON subscriber(suname) WHERE suname IS NOT NULL; |
| 60 | 65 | @ |
| 61 | 66 | @ -- Email notifications that need to be sent. |
| 62 | 67 | @ -- |
| 63 | | -@ -- If the eventid key is an integer, then it corresponds to the |
| 64 | | -@ -- EVENT.OBJID table. Other kinds of eventids are reserved for |
| 65 | | -@ -- future expansion. |
| 68 | +@ -- The first character of the eventid determines the event type. |
| 69 | +@ -- Remaining characters determine the specific event. For example, |
| 70 | +@ -- 'c4413' means check-in with rid=4413. |
| 66 | 71 | @ -- |
| 67 | | -@ CREATE TABLE repository.email_pending( |
| 68 | | -@ eventid ANY PRIMARY KEY, -- Object that changed |
| 72 | +@ CREATE TABLE repository.pending_alert( |
| 73 | +@ eventid TEXT PRIMARY KEY, -- Object that changed |
| 69 | 74 | @ sentSep BOOLEAN DEFAULT false, -- individual emails sent |
| 70 | | -@ sentDigest BOOLEAN DEFAULT false -- digest emails sent |
| 75 | +@ sendDigest BOOLEAN DEFAULT false -- digest emails sent |
| 71 | 76 | @ ) WITHOUT ROWID; |
| 72 | 77 | @ |
| 73 | 78 | @ -- Record bounced emails. If too many bounces are received within |
| 74 | 79 | @ -- some defined time range, then cancel the subscription. Older |
| 75 | 80 | @ -- entries are periodically purged. |
| | @@ -85,10 +90,66 @@ |
| 85 | 90 | ** Make sure the unversioned table exists in the repository. |
| 86 | 91 | */ |
| 87 | 92 | void email_schema(void){ |
| 88 | 93 | if( !db_table_exists("repository", "subscriber") ){ |
| 89 | 94 | db_multi_exec(zEmailInit/*works-like:""*/); |
| 95 | + email_triggers_enable(); |
| 96 | + } |
| 97 | +} |
| 98 | + |
| 99 | +/* |
| 100 | +** Enable triggers that automatically populate the event_pending |
| 101 | +** table. |
| 102 | +*/ |
| 103 | +void email_triggers_enable(void){ |
| 104 | + if( !db_table_exists("repository","pending_alert") ) return; |
| 105 | + db_multi_exec( |
| 106 | + "CREATE TRIGGER IF NOT EXISTS repository.email_trigger1\n" |
| 107 | + "AFTER INSERT ON event BEGIN\n" |
| 108 | + " INSERT INTO pending_alert(eventid,mtime)\n" |
| 109 | + " SELECT printf('%%.1c%%d',new.type,new.objid)," |
| 110 | + " julianday('now') WHERE true\n" |
| 111 | + " ON CONFLICT(eventId) DO NOTHING;\n" |
| 112 | + "END;" |
| 113 | + ); |
| 114 | +} |
| 115 | + |
| 116 | +/* |
| 117 | +** Disable triggers the event_pending triggers. |
| 118 | +** |
| 119 | +** This must be called before rebuilding the EVENT table, for example |
| 120 | +** via the "fossil rebuild" command. |
| 121 | +*/ |
| 122 | +void email_triggers_disable(void){ |
| 123 | + db_multi_exec( |
| 124 | + "DROP TRIGGER IF EXISTS repository.email_trigger1;\n" |
| 125 | + ); |
| 126 | +} |
| 127 | + |
| 128 | +/* |
| 129 | +** Return true if email alerts are active. |
| 130 | +*/ |
| 131 | +int email_enabled(void){ |
| 132 | + if( !db_table_exists("repository", "subscriber") ) return 0; |
| 133 | + if( fossil_strcmp(db_get("email-send-method","off"),"off")==0 ) return 0; |
| 134 | + return 1; |
| 135 | +} |
| 136 | + |
| 137 | + |
| 138 | + |
| 139 | +/* |
| 140 | +** Insert a "Subscriber List" submenu link if the current user |
| 141 | +** is an administrator. |
| 142 | +*/ |
| 143 | +void email_submenu_common(void){ |
| 144 | + if( g.perm.Admin ){ |
| 145 | + if( fossil_strcmp(g.zPath,"subscribers") ){ |
| 146 | + style_submenu_element("List Subscribers","%R/subscribers"); |
| 147 | + } |
| 148 | + if( fossil_strcmp(g.zPath,"subscribe") ){ |
| 149 | + style_submenu_element("Add New Subscriber","%R/subscribe"); |
| 150 | + } |
| 90 | 151 | } |
| 91 | 152 | } |
| 92 | 153 | |
| 93 | 154 | |
| 94 | 155 | /* |
| | @@ -99,64 +160,96 @@ |
| 99 | 160 | void setup_email(void){ |
| 100 | 161 | static const char *const azSendMethods[] = { |
| 101 | 162 | "off", "Disabled", |
| 102 | 163 | "pipe", "Pipe to a command", |
| 103 | 164 | "db", "Store in a database", |
| 104 | | - "file", "Store in a directory" |
| 165 | + "dir", "Store in a directory" |
| 105 | 166 | }; |
| 106 | 167 | login_check_credentials(); |
| 107 | 168 | if( !g.perm.Setup ){ |
| 108 | 169 | login_needed(0); |
| 109 | 170 | return; |
| 110 | 171 | } |
| 111 | 172 | db_begin_transaction(); |
| 112 | 173 | |
| 174 | + email_submenu_common(); |
| 113 | 175 | style_header("Email Notification Setup"); |
| 114 | 176 | @ <form action="%R/setup_email" method="post"><div> |
| 115 | 177 | @ <input type="submit" name="submit" value="Apply Changes" /><hr> |
| 116 | 178 | login_insert_csrf_secret(); |
| 117 | | - multiple_choice_attribute("Email Send Method","email-send-method", |
| 118 | | - "esm", "off", count(azSendMethods)/2, azSendMethods); |
| 179 | + |
| 180 | + entry_attribute("Canonical Server URL", 40, "email-url", |
| 181 | + "eurl", "", 0); |
| 182 | + @ <p><b>Required.</b> |
| 183 | + @ This is URL used as the basename for hyperlinks included in |
| 184 | + @ email alert text. Omit the trailing "/". |
| 185 | + @ Suggested value: "%h(g.zBaseURL)" |
| 186 | + @ (Property: "email-url")</p> |
| 187 | + @ <hr> |
| 188 | + |
| 189 | + entry_attribute("\"From\" email address", 20, "email-self", |
| 190 | + "eself", "", 0); |
| 191 | + @ <p><b>Required.</b> |
| 192 | + @ This is the email from which email notifications are sent. The |
| 193 | + @ system administrator should arrange for emails sent to this address |
| 194 | + @ to be handed off to the "fossil email incoming" command so that Fossil |
| 195 | + @ can handle bounces. (Property: "email-self")</p> |
| 196 | + @ <hr> |
| 197 | + |
| 198 | + entry_attribute("Repository Nickname", 16, "email-subname", |
| 199 | + "enn", "", 0); |
| 200 | + @ <p><b>Required.</b> |
| 201 | + @ This is short name used to identifies the repository in the |
| 202 | + @ Subject: line of email alerts. Traditionally this name is |
| 203 | + @ included in square brackets. Examples: "[fossil-src]", "[sqlite-src]". |
| 204 | + @ (Property: "email-subname")</p> |
| 205 | + @ <hr> |
| 206 | + |
| 207 | + multiple_choice_attribute("Email Send Method", "email-send-method", "esm", |
| 208 | + "off", count(azSendMethods)/2, azSendMethods); |
| 119 | 209 | @ <p>How to send email. The "Pipe to a command" |
| 120 | 210 | @ method is the usual choice in production. |
| 121 | 211 | @ (Property: "email-send-method")</p> |
| 122 | 212 | @ <hr> |
| 123 | | - entry_attribute("Command To Pipe Email To", 80, "esc", |
| 124 | | - "email-send-command", "sendmail -t", 0); |
| 213 | + |
| 214 | + |
| 215 | + entry_attribute("Command To Pipe Email To", 80, "email-send-command", |
| 216 | + "ecmd", "sendmail -t", 0); |
| 125 | 217 | @ <p>When the send method is "pipe to a command", this is the command |
| 126 | 218 | @ that is run. Email messages are piped into the standard input of this |
| 127 | 219 | @ command. The command is expected to extract the sender address, |
| 128 | 220 | @ recepient addresses, and subject from the header of the piped email |
| 129 | 221 | @ text. (Property: "email-send-command")</p> |
| 130 | 222 | |
| 131 | | - entry_attribute("Database In Which To Store Email", 60, "esdb", |
| 132 | | - "email-send-db", "", 0); |
| 223 | + entry_attribute("Database In Which To Store Email", 60, "email-send-db", |
| 224 | + "esdb", "", 0); |
| 133 | 225 | @ <p>When the send method is "store in a databaes", each email message is |
| 134 | 226 | @ stored in an SQLite database file with the name given here. |
| 135 | 227 | @ (Property: "email-send-db")</p> |
| 136 | 228 | |
| 137 | | - entry_attribute("Directory In Which To Store Email", 60, "esdir", |
| 138 | | - "email-send-dir", "", 0); |
| 229 | + entry_attribute("Directory In Which To Store Email", 60, "email-send-dir", |
| 230 | + "esdir", "", 0); |
| 139 | 231 | @ <p>When the send method is "store in a directory", each email message is |
| 140 | 232 | @ stored as a separate file in the directory shown here. |
| 141 | 233 | @ (Property: "email-send-dir")</p> |
| 142 | 234 | @ <hr> |
| 143 | 235 | |
| 144 | | - entry_attribute("\"From\" email address", 40, "ef", |
| 145 | | - "email-self", "", 0); |
| 146 | | - @ <p>This is the email from which email notifications are sent. The |
| 147 | | - @ system administrator should arrange for emails sent to this address |
| 148 | | - @ to be handed off to the "fossil email incoming" command so that Fossil |
| 149 | | - @ can handle bounces. (Property: "email-self")</p> |
| 150 | | - @ <hr> |
| 151 | | - |
| 152 | | - entry_attribute("Administrator email address", 40, "ea", |
| 153 | | - "email-admin", "", 0); |
| 236 | + entry_attribute("Administrator email address", 40, "email-admin", |
| 237 | + "eadmin", "", 0); |
| 154 | 238 | @ <p>This is the email for the human administrator for the system. |
| 155 | 239 | @ Abuse and trouble reports are send here. |
| 156 | 240 | @ (Property: "email-admin")</p> |
| 157 | 241 | @ <hr> |
| 242 | + |
| 243 | + entry_attribute("Inbound email directory", 40, "email-receive-dir", |
| 244 | + "erdir", "", 0); |
| 245 | + @ <p>Inbound emails can be stored in a directory for analysis as |
| 246 | + @ a debugging aid. Put the name of that directory in this entry box. |
| 247 | + @ Disable saving of inbound email by making this an empty string. |
| 248 | + @ Abuse and trouble reports are send here. |
| 249 | + @ (Property: "email-receive-dir")</p> |
| 250 | + @ <hr> |
| 158 | 251 | @ <p><input type="submit" name="submit" value="Apply Changes" /></p> |
| 159 | 252 | @ </div></form> |
| 160 | 253 | db_end_transaction(0); |
| 161 | 254 | style_footer(); |
| 162 | 255 | } |
| | @@ -172,10 +265,23 @@ |
| 172 | 265 | k = translateBase64(blob_buffer(pMsg)+i, i+54<n ? 54 : n-i, zBuf); |
| 173 | 266 | blob_append(pOut, zBuf, k); |
| 174 | 267 | blob_append(pOut, "\r\n", 2); |
| 175 | 268 | } |
| 176 | 269 | } |
| 270 | + |
| 271 | +/* |
| 272 | +** Come up with a unique filename in the zDir directory. |
| 273 | +** |
| 274 | +** Space to hold the filename is obtained from mprintf() and must |
| 275 | +** be freed using fossil_free() by the caller. |
| 276 | +*/ |
| 277 | +static char *emailTempFilename(const char *zDir){ |
| 278 | + char *zFile = db_text(0, |
| 279 | + "SELECT %Q||strftime('/%%Y%%m%%d%%H%%M%%S-','now')||hex(randomblob(8))", |
| 280 | + zDir); |
| 281 | + return zFile; |
| 282 | +} |
| 177 | 283 | |
| 178 | 284 | #if defined(_WIN32) || defined(WIN32) |
| 179 | 285 | # undef popen |
| 180 | 286 | # define popen _popen |
| 181 | 287 | # undef pclose |
| | @@ -231,18 +337,20 @@ |
| 231 | 337 | zBoundary = db_text(0, "SELECT hex(randomblob(20))"); |
| 232 | 338 | blob_appendf(&all, "Content-Type: multipart/alternative;" |
| 233 | 339 | " boundary=\"%s\"\r\n", zBoundary); |
| 234 | 340 | } |
| 235 | 341 | if( pPlain ){ |
| 342 | + blob_add_final_newline(pPlain); |
| 236 | 343 | if( zBoundary ){ |
| 237 | 344 | blob_appendf(&all, "\r\n--%s\r\n", zBoundary); |
| 238 | 345 | } |
| 239 | 346 | blob_appendf(&all,"Content-Type: text/plain\r\n"); |
| 240 | 347 | blob_appendf(&all, "Content-Transfer-Encoding: base64\r\n\r\n"); |
| 241 | 348 | append_base64(&all, pPlain); |
| 242 | 349 | } |
| 243 | 350 | if( pHtml ){ |
| 351 | + blob_add_final_newline(pHtml); |
| 244 | 352 | if( zBoundary ){ |
| 245 | 353 | blob_appendf(&all, "--%s\r\n", zBoundary); |
| 246 | 354 | } |
| 247 | 355 | blob_appendf(&all,"Content-Type: text/html\r\n"); |
| 248 | 356 | blob_appendf(&all, "Content-Transfer-Encoding: base64\r\n\r\n"); |
| | @@ -281,20 +389,32 @@ |
| 281 | 389 | fclose(out); |
| 282 | 390 | } |
| 283 | 391 | } |
| 284 | 392 | }else if( strcmp(zDest, "dir")==0 ){ |
| 285 | 393 | const char *zDir = db_get("email-send-dir","./"); |
| 286 | | - char *zFile = db_text(0, |
| 287 | | - "SELECT %Q||strftime('/%%Y%%m%%d%%H%%M%%S','now')||hex(randomblob(8))", |
| 288 | | - zDir); |
| 394 | + char *zFile = emailTempFilename(zDir); |
| 289 | 395 | blob_write_to_file(&all, zFile); |
| 290 | 396 | fossil_free(zFile); |
| 291 | 397 | }else if( strcmp(zDest, "stdout")==0 ){ |
| 292 | 398 | fossil_print("%s\n", blob_str(&all)); |
| 293 | 399 | } |
| 294 | 400 | blob_zero(&all); |
| 295 | 401 | } |
| 402 | + |
| 403 | +/* |
| 404 | +** Analyze and act on a received email. |
| 405 | +** |
| 406 | +** This routine takes ownership of the Blob parameter and is responsible |
| 407 | +** for freeing that blob when it is done with it. |
| 408 | +** |
| 409 | +** This routine acts on all email messages received from the |
| 410 | +** "fossil email inbound" command. |
| 411 | +*/ |
| 412 | +void email_receive(Blob *pMsg){ |
| 413 | + /* To Do: Look for bounce messages and possibly disable subscriptions */ |
| 414 | + blob_zero(pMsg); |
| 415 | +} |
| 296 | 416 | |
| 297 | 417 | /* |
| 298 | 418 | ** SETTING: email-send-method width=5 default=off |
| 299 | 419 | ** Determine the method used to send email. Allowed values are |
| 300 | 420 | ** "off", "pipe", "dir", "db", and "stdout". The "off" value means |
| | @@ -326,19 +446,37 @@ |
| 326 | 446 | /* |
| 327 | 447 | ** SETTING: email-self width=40 |
| 328 | 448 | ** This is the email address for the repository. Outbound emails add |
| 329 | 449 | ** this email address as the "From:" field. |
| 330 | 450 | */ |
| 451 | +/* |
| 452 | +** SETTING: email-receive-dir width=40 |
| 453 | +** Inbound email messages are saved as separate files in this directory, |
| 454 | +** for debugging analysis. Disable saving of inbound emails omitting |
| 455 | +** this setting, or making it an empty string. |
| 456 | +*/ |
| 331 | 457 | |
| 332 | 458 | |
| 333 | 459 | /* |
| 334 | 460 | ** COMMAND: email |
| 335 | 461 | ** |
| 336 | 462 | ** Usage: %fossil email SUBCOMMAND ARGS... |
| 337 | 463 | ** |
| 338 | 464 | ** Subcommands: |
| 339 | 465 | ** |
| 466 | +** exec Compose and send pending email alerts. |
| 467 | +** Some installations may want to do this via |
| 468 | +** a cron-job to make sure alerts are sent |
| 469 | +** in a timely manner. |
| 470 | +** Options: |
| 471 | +** |
| 472 | +** --digest Send digests |
| 473 | +** |
| 474 | +** inbound [FILE] Receive an inbound email message. This message |
| 475 | +** is analyzed to see if it is a bounce, and if |
| 476 | +** necessary, subscribers may be disabled. |
| 477 | +** |
| 340 | 478 | ** reset Hard reset of all email notification tables |
| 341 | 479 | ** in the repository. This erases all subscription |
| 342 | 480 | ** information. Use with extreme care. |
| 343 | 481 | ** |
| 344 | 482 | ** send TO [OPTIONS] Send a single email message using whatever |
| | @@ -351,37 +489,71 @@ |
| 351 | 489 | ** --stdout |
| 352 | 490 | ** --subject|-S SUBJECT |
| 353 | 491 | ** |
| 354 | 492 | ** settings [NAME VALUE] With no arguments, list all email settings. |
| 355 | 493 | ** Or change the value of a single email setting. |
| 494 | +** |
| 495 | +** subscribers [PATTERN] List all subscribers matching PATTERN. |
| 496 | +** |
| 497 | +** unsubscribe EMAIL Remove a single subscriber with the given EMAIL. |
| 356 | 498 | */ |
| 357 | 499 | void email_cmd(void){ |
| 358 | 500 | const char *zCmd; |
| 359 | 501 | int nCmd; |
| 360 | 502 | db_find_and_open_repository(0, 0); |
| 361 | 503 | email_schema(); |
| 362 | 504 | zCmd = g.argc>=3 ? g.argv[2] : "x"; |
| 363 | 505 | nCmd = (int)strlen(zCmd); |
| 506 | + if( strncmp(zCmd, "exec", nCmd)==0 ){ |
| 507 | + u32 eFlags = 0; |
| 508 | + if( find_option("digest",0,0)!=0 ) eFlags |= SENDALERT_DIGEST; |
| 509 | + verify_all_options(); |
| 510 | + email_send_alerts(eFlags); |
| 511 | + }else |
| 512 | + if( strncmp(zCmd, "inbound", nCmd)==0 ){ |
| 513 | + Blob email; |
| 514 | + const char *zInboundDir = db_get("email-receive-dir",""); |
| 515 | + verify_all_options(); |
| 516 | + if( g.argc!=3 && g.argc!=4 ){ |
| 517 | + usage("inbound [FILE]"); |
| 518 | + } |
| 519 | + blob_read_from_file(&email, g.argc==3 ? "-" : g.argv[3], ExtFILE); |
| 520 | + if( zInboundDir[0] ){ |
| 521 | + char *zFN = emailTempFilename(zInboundDir); |
| 522 | + blob_write_to_file(&email, zFN); |
| 523 | + fossil_free(zFN); |
| 524 | + } |
| 525 | + email_receive(&email); |
| 526 | + }else |
| 364 | 527 | if( strncmp(zCmd, "reset", nCmd)==0 ){ |
| 365 | | - Blob yn; |
| 366 | 528 | int c; |
| 367 | | - fossil_print( |
| 368 | | - "This will erase all content in the repository tables, thus\n" |
| 369 | | - "deleting all subscriber information. The information will be\n" |
| 370 | | - "unrecoverable.\n"); |
| 371 | | - prompt_user("Continue? (y/N) ", &yn); |
| 372 | | - c = blob_str(&yn)[0]; |
| 529 | + int bForce = find_option("force","f",0)!=0; |
| 530 | + verify_all_options(); |
| 531 | + if( bForce ){ |
| 532 | + c = 'y'; |
| 533 | + }else{ |
| 534 | + Blob yn; |
| 535 | + fossil_print( |
| 536 | + "This will erase all content in the repository tables, thus\n" |
| 537 | + "deleting all subscriber information. The information will be\n" |
| 538 | + "unrecoverable.\n"); |
| 539 | + prompt_user("Continue? (y/N) ", &yn); |
| 540 | + c = blob_str(&yn)[0]; |
| 541 | + blob_zero(&yn); |
| 542 | + } |
| 373 | 543 | if( c=='y' ){ |
| 544 | + email_triggers_disable(); |
| 374 | 545 | db_multi_exec( |
| 375 | 546 | "DROP TABLE IF EXISTS subscriber;\n" |
| 376 | | - "DROP TABLE IF EXISTS subscription;\n" |
| 547 | + "DROP TABLE IF EXISTS pending_alert;\n" |
| 548 | + "DROP TABLE IF EXISTS email_bounce;\n" |
| 549 | + /* Legacy */ |
| 377 | 550 | "DROP TABLE IF EXISTS email_pending;\n" |
| 378 | | - "DROP TABLE IF EXISTS email_bounce;\n" |
| 551 | + "DROP TABLE IF EXISTS subscription;\n" |
| 379 | 552 | ); |
| 380 | 553 | email_schema(); |
| 381 | 554 | } |
| 382 | | - blob_zero(&yn); |
| 383 | 555 | }else |
| 384 | 556 | if( strncmp(zCmd, "send", nCmd)==0 ){ |
| 385 | 557 | Blob prompt, body, hdr; |
| 386 | 558 | int sendAsBoth = find_option("both",0,0)!=0; |
| 387 | 559 | int sendAsHtml = find_option("html",0,0)!=0; |
| | @@ -402,10 +574,11 @@ |
| 402 | 574 | if( zSource ){ |
| 403 | 575 | blob_read_from_file(&body, zSource, ExtFILE); |
| 404 | 576 | }else{ |
| 405 | 577 | prompt_for_user_comment(&body, &prompt); |
| 406 | 578 | } |
| 579 | + blob_add_final_newline(&body); |
| 407 | 580 | if( sendAsHtml ){ |
| 408 | 581 | email_send(&hdr, 0, &body, zDest); |
| 409 | 582 | }else if( sendAsBoth ){ |
| 410 | 583 | Blob html; |
| 411 | 584 | blob_init(&html, 0, 0); |
| | @@ -416,14 +589,13 @@ |
| 416 | 589 | email_send(&hdr, &body, 0, zDest); |
| 417 | 590 | } |
| 418 | 591 | blob_zero(&hdr); |
| 419 | 592 | blob_zero(&body); |
| 420 | 593 | blob_zero(&prompt); |
| 421 | | - } |
| 422 | | - else if( strncmp(zCmd, "settings", nCmd)==0 ){ |
| 594 | + }else |
| 595 | + if( strncmp(zCmd, "settings", nCmd)==0 ){ |
| 423 | 596 | int isGlobal = find_option("global",0,0)!=0; |
| 424 | | - int i; |
| 425 | 597 | int nSetting; |
| 426 | 598 | const Setting *pSetting = setting_info(&nSetting); |
| 427 | 599 | db_open_config(1, 0); |
| 428 | 600 | verify_all_options(); |
| 429 | 601 | if( g.argc!=3 && g.argc!=5 ) usage("setting [NAME VALUE]"); |
| | @@ -439,15 +611,133 @@ |
| 439 | 611 | pSetting = setting_info(&nSetting); |
| 440 | 612 | for(; nSetting>0; nSetting--, pSetting++ ){ |
| 441 | 613 | if( strncmp(pSetting->name,"email-",6)!=0 ) continue; |
| 442 | 614 | print_setting(pSetting); |
| 443 | 615 | } |
| 616 | + }else |
| 617 | + if( strncmp(zCmd, "subscribers", nCmd)==0 ){ |
| 618 | + Stmt q; |
| 619 | + verify_all_options(); |
| 620 | + if( g.argc!=3 && g.argc!=4 ) usage("subscribers [PATTERN]"); |
| 621 | + if( g.argc==4 ){ |
| 622 | + char *zPattern = g.argv[3]; |
| 623 | + db_prepare(&q, |
| 624 | + "SELECT semail FROM subscriber" |
| 625 | + " WHERE semail LIKE '%%%q%%' OR suname LIKE '%%%q%%'" |
| 626 | + " OR semail GLOB '*%q*' or suname GLOB '*%q*'" |
| 627 | + " ORDER BY semail", |
| 628 | + zPattern, zPattern, zPattern, zPattern); |
| 629 | + }else{ |
| 630 | + db_prepare(&q, |
| 631 | + "SELECT semail FROM subscriber" |
| 632 | + " ORDER BY semail"); |
| 633 | + } |
| 634 | + while( db_step(&q)==SQLITE_ROW ){ |
| 635 | + fossil_print("%s\n", db_column_text(&q, 0)); |
| 636 | + } |
| 637 | + db_finalize(&q); |
| 638 | + }else |
| 639 | + if( strncmp(zCmd, "unsubscribe", nCmd)==0 ){ |
| 640 | + verify_all_options(); |
| 641 | + if( g.argc!=4 ) usage("unsubscribe EMAIL"); |
| 642 | + db_multi_exec( |
| 643 | + "DELETE FROM subscriber WHERE semail=%Q", g.argv[3]); |
| 644 | + }else |
| 645 | + { |
| 646 | + usage("exec|inbound|reset|send|setting|subscribers|unsubscribe"); |
| 647 | + } |
| 648 | +} |
| 649 | + |
| 650 | +/* |
| 651 | +** Do error checking on a submitted subscription form. Return TRUE |
| 652 | +** if the submission is valid. Return false if any problems are seen. |
| 653 | +*/ |
| 654 | +static int subscribe_error_check( |
| 655 | + int *peErr, /* Type of error */ |
| 656 | + char **pzErr, /* Error message text */ |
| 657 | + int needCaptcha /* True if captcha check needed */ |
| 658 | +){ |
| 659 | + const char *zEAddr; |
| 660 | + int i, j, n; |
| 661 | + char c; |
| 662 | + |
| 663 | + *peErr = 0; |
| 664 | + *pzErr = 0; |
| 665 | + |
| 666 | + /* Check the validity of the email address. |
| 667 | + ** |
| 668 | + ** (1) Exactly one '@' character. |
| 669 | + ** (2) No other characters besides [a-zA-Z0-9._-] |
| 670 | + */ |
| 671 | + zEAddr = P("e"); |
| 672 | + if( zEAddr==0 ) return 0; |
| 673 | + for(i=j=0; (c = zEAddr[i])!=0; i++){ |
| 674 | + if( c=='@' ){ |
| 675 | + n = i; |
| 676 | + j++; |
| 677 | + continue; |
| 678 | + } |
| 679 | + if( !fossil_isalnum(c) && c!='.' && c!='_' && c!='-' ){ |
| 680 | + *peErr = 1; |
| 681 | + *pzErr = mprintf("illegal character in email address: 0x%x '%c'", |
| 682 | + c, c); |
| 683 | + return 0; |
| 684 | + } |
| 685 | + } |
| 686 | + if( j!=1 ){ |
| 687 | + *peErr = 1; |
| 688 | + *pzErr = mprintf("email address should contain exactly one '@'"); |
| 689 | + return 0; |
| 690 | + } |
| 691 | + if( n<1 ){ |
| 692 | + *peErr = 1; |
| 693 | + *pzErr = mprintf("name missing before '@' in email address"); |
| 694 | + return 0; |
| 695 | + } |
| 696 | + if( n>i-5 ){ |
| 697 | + *peErr = 1; |
| 698 | + *pzErr = mprintf("email domain too short"); |
| 699 | + return 0; |
| 700 | + } |
| 701 | + |
| 702 | + /* Verify the captcha */ |
| 703 | + if( needCaptcha && !captcha_is_correct(1) ){ |
| 704 | + *peErr = 2; |
| 705 | + *pzErr = mprintf("incorrect security code"); |
| 706 | + return 0; |
| 444 | 707 | } |
| 445 | | - else{ |
| 446 | | - usage("reset|send|setting"); |
| 708 | + |
| 709 | + /* Check to make sure the email address is available for reuse */ |
| 710 | + if( db_exists("SELECT 1 FROM subscriber WHERE semail=%Q", zEAddr) ){ |
| 711 | + *peErr = 1; |
| 712 | + *pzErr = mprintf("this email address is used by someone else"); |
| 713 | + return 0; |
| 447 | 714 | } |
| 715 | + |
| 716 | + /* If we reach this point, all is well */ |
| 717 | + return 1; |
| 448 | 718 | } |
| 719 | + |
| 720 | +/* |
| 721 | +** Text of email message sent in order to confirm a subscription. |
| 722 | +*/ |
| 723 | +static const char zConfirmMsg[] = |
| 724 | +@ Someone has signed you up for email alerts on the Fossil repository |
| 725 | +@ at %s. |
| 726 | +@ |
| 727 | +@ To confirm your subscription and begin receiving alerts, click on |
| 728 | +@ the following hyperlink: |
| 729 | +@ |
| 730 | +@ %s/alerts/%s |
| 731 | +@ |
| 732 | +@ Save the hyperlink above! You can reuse this same hyperlink to |
| 733 | +@ unsubscribe or to change the kinds of alerts you receive. |
| 734 | +@ |
| 735 | +@ If you do not want to subscribe, you can simply ignore this message. |
| 736 | +@ You will not be contacted again. |
| 737 | +@ |
| 738 | +; |
| 449 | 739 | |
| 450 | 740 | /* |
| 451 | 741 | ** WEBPAGE: subscribe |
| 452 | 742 | ** |
| 453 | 743 | ** Allow users to subscribe to email notifications, or to change or |
| | @@ -455,65 +745,162 @@ |
| 455 | 745 | */ |
| 456 | 746 | void subscribe_page(void){ |
| 457 | 747 | int needCaptcha; |
| 458 | 748 | unsigned int uSeed; |
| 459 | 749 | const char *zDecoded; |
| 460 | | - char *zCaptcha; |
| 750 | + char *zCaptcha = 0; |
| 751 | + char *zErr = 0; |
| 752 | + int eErr = 0; |
| 461 | 753 | |
| 462 | 754 | login_check_credentials(); |
| 463 | 755 | if( !g.perm.EmailAlert ){ |
| 464 | 756 | login_needed(g.anon.EmailAlert); |
| 465 | 757 | return; |
| 466 | 758 | } |
| 467 | | - style_header("Email Subscription"); |
| 468 | | - needCaptcha = P("usecaptcha")!=0 || login_is_nobody() |
| 469 | | - || login_is_special(g.zLogin); |
| 759 | + if( login_is_individual() |
| 760 | + && db_exists("SELECT 1 FROM subscriber WHERE suname=%Q",g.zLogin) |
| 761 | + ){ |
| 762 | + /* This person is already signed up for email alerts. Jump |
| 763 | + ** to the screen that lets them edit their alert preferences. |
| 764 | + ** Except, administrators can create subscriptions for others so |
| 765 | + ** do not jump for them. |
| 766 | + */ |
| 767 | + if( g.perm.Admin ){ |
| 768 | + /* Admins get a link to admin their own account, but they |
| 769 | + ** stay on this page so that they can create subscriptions |
| 770 | + ** for other people. */ |
| 771 | + style_submenu_element("My Subscription","%R/alerts"); |
| 772 | + }else{ |
| 773 | + /* Everybody else jumps to the page to administer their own |
| 774 | + ** account only. */ |
| 775 | + cgi_redirectf("%R/alerts"); |
| 776 | + return; |
| 777 | + } |
| 778 | + } |
| 779 | + email_submenu_common(); |
| 780 | + needCaptcha = !login_is_individual(); |
| 781 | + if( P("submit") |
| 782 | + && cgi_csrf_safe(1) |
| 783 | + && subscribe_error_check(&eErr,&zErr,needCaptcha) |
| 784 | + ){ |
| 785 | + /* A validated request for a new subscription has been received. */ |
| 786 | + char ssub[20]; |
| 787 | + const char *zEAddr = P("e"); |
| 788 | + sqlite3_int64 id; /* New subscriber Id */ |
| 789 | + const char *zCode; /* New subscriber code (in hex) */ |
| 790 | + int nsub = 0; |
| 791 | + const char *suname = PT("suname"); |
| 792 | + if( suname==0 && needCaptcha==0 && !g.perm.Admin ) suname = g.zLogin; |
| 793 | + if( suname && suname[0]==0 ) suname = 0; |
| 794 | + if( PB("sa") ) ssub[nsub++] = 'a'; |
| 795 | + if( PB("sc") ) ssub[nsub++] = 'c'; |
| 796 | + if( PB("st") ) ssub[nsub++] = 't'; |
| 797 | + if( PB("sw") ) ssub[nsub++] = 'w'; |
| 798 | + ssub[nsub] = 0; |
| 799 | + db_multi_exec( |
| 800 | + "INSERT INTO subscriber(subscriberCode,semail,suname," |
| 801 | + " sverified,sdonotcall,sdigest,ssub,sctime,smtime,smip)" |
| 802 | + "VALUES(randomblob(32),%Q,%Q,%d,0,%d,%Q," |
| 803 | + " julianday('now'),julianday('now'),%Q)", |
| 804 | + /* semail */ zEAddr, |
| 805 | + /* suname */ suname, |
| 806 | + /* sverified */ needCaptcha==0, |
| 807 | + /* sdigest */ PB("di"), |
| 808 | + /* ssub */ ssub, |
| 809 | + /* smip */ g.zIpAddr |
| 810 | + ); |
| 811 | + id = db_last_insert_rowid(); |
| 812 | + zCode = db_text(0, |
| 813 | + "SELECT hex(subscriberCode) FROM subscriber WHERE subscriberId=%lld", |
| 814 | + id); |
| 815 | + if( !needCaptcha ){ |
| 816 | + /* The new subscription has been added on behalf of a logged-in user. |
| 817 | + ** No verification is required. Jump immediately to /alerts page. |
| 818 | + */ |
| 819 | + cgi_redirectf("%R/alerts/%s", zCode); |
| 820 | + return; |
| 821 | + }else{ |
| 822 | + /* We need to send a verification email */ |
| 823 | + Blob hdr, body; |
| 824 | + blob_init(&hdr,0,0); |
| 825 | + blob_init(&body,0,0); |
| 826 | + blob_appendf(&hdr, "To: %s\n", zEAddr); |
| 827 | + blob_appendf(&hdr, "Subject: Subscription verification\n"); |
| 828 | + blob_appendf(&body, zConfirmMsg/*works-like:"%s%s%s"*/, |
| 829 | + g.zBaseURL, g.zBaseURL, zCode); |
| 830 | + email_send(&hdr, &body, 0, 0); |
| 831 | + style_header("Email Alert Verification"); |
| 832 | + @ <p>An email has been sent to "%h(zEAddr)". That email contains a |
| 833 | + @ hyperlink that you must click on in order to activate your |
| 834 | + @ subscription.</p> |
| 835 | + style_footer(); |
| 836 | + } |
| 837 | + return; |
| 838 | + } |
| 839 | + style_header("Signup For Email Alerts"); |
| 840 | + @ <p>To receive email notifications for changes to this |
| 841 | + @ repository, fill out the form below and press "Submit" button.</p> |
| 470 | 842 | form_begin(0, "%R/subscribe"); |
| 471 | 843 | @ <table class="subscribe"> |
| 472 | 844 | @ <tr> |
| 473 | 845 | @ <td class="form_label">Email Address:</td> |
| 474 | | - @ <td><input type="text" name="e" value="" size="30"></td> |
| 475 | | - @ <td></td> |
| 846 | + @ <td><input type="text" name="e" value="%h(PD("e",""))" size="30"></td> |
| 847 | + if( eErr==1 ){ |
| 848 | + @ <td><span class="loginError">← %h(zErr)</span></td> |
| 849 | + } |
| 476 | 850 | @ </tr> |
| 477 | 851 | if( needCaptcha ){ |
| 478 | 852 | uSeed = captcha_seed(); |
| 479 | 853 | zDecoded = captcha_decode(uSeed); |
| 480 | 854 | zCaptcha = captcha_render(zDecoded); |
| 481 | 855 | @ <tr> |
| 482 | 856 | @ <td class="form_label">Security Code:</td> |
| 483 | 857 | @ <td><input type="text" name="captcha" value="" size="30"> |
| 484 | | - @ <input type="hidden" name="usecaptcha" value="1"></td> |
| 485 | | - @ <input type="hidden" name="captchaseed" value="%u(uSeed)"></td> |
| 486 | | - @ <td><span class="optionalTag">(copy from below)</span></td> |
| 487 | | - @ </tr> |
| 488 | | - } |
| 489 | | - @ <tr> |
| 490 | | - @ <td class="form_label">Nickname:</td> |
| 491 | | - @ <td><input type="text" name="nn" value="" size="30"></td> |
| 492 | | - @ <td><span class="optionalTag">(optional)</span></td> |
| 493 | | - @ </tr> |
| 494 | | - @ <tr> |
| 495 | | - @ <td class="form_label">Password:</td> |
| 496 | | - @ <td><input type="password" name="pw" value="" size="30"></td> |
| 497 | | - @ <td><span class="optionalTag">(optional)</span></td> |
| 498 | | - @ </tr> |
| 499 | | - @ <tr> |
| 500 | | - @ <td class="form_label">Options:</td> |
| 501 | | - @ <td><label><input type="checkbox" name="sa" value="0">\ |
| 502 | | - @ Announcements</label><br> |
| 503 | | - @ <label><input type="checkbox" name="sc" value="0">\ |
| 504 | | - @ Check-ins</label><br> |
| 505 | | - @ <label><input type="checkbox" name="st" value="0">\ |
| 506 | | - @ Ticket changes</label><br> |
| 507 | | - @ <label><input type="checkbox" name="sw" value="0">\ |
| 508 | | - @ Wiki</label><br> |
| 509 | | - @ <label><input type="checkbox" name="di" value="0">\ |
| 510 | | - @ Daily digest only</label><br></td> |
| 858 | + @ <input type="hidden" name="captchaseed" value="%u(uSeed)"></td> |
| 859 | + if( eErr==2 ){ |
| 860 | + @ <td><span class="loginError">← %h(zErr)</span></td> |
| 861 | + } |
| 862 | + @ </tr> |
| 863 | + } |
| 864 | + if( g.perm.Admin ){ |
| 865 | + @ <tr> |
| 866 | + @ <td class="form_label">User:</td> |
| 867 | + @ <td><input type="text" name="suname" value="%h(PD("suname",g.zLogin))" \ |
| 868 | + @ size="30"></td> |
| 869 | + if( eErr==3 ){ |
| 870 | + @ <td><span class="loginError">← %h(zErr)</span></td> |
| 871 | + } |
| 872 | + @ </tr> |
| 873 | + } |
| 874 | + @ <tr> |
| 875 | + @ <td class="form_label">Options:</td> |
| 876 | + @ <td><label><input type="checkbox" name="sa" %s(PCK("sa"))> \ |
| 877 | + @ Announcements</label><br> |
| 878 | + @ <label><input type="checkbox" name="sc" %s(PCK("sc"))> \ |
| 879 | + @ Check-ins</label><br> |
| 880 | + @ <label><input type="checkbox" name="st" %s(PCK("st"))> \ |
| 881 | + @ Ticket changes</label><br> |
| 882 | + @ <label><input type="checkbox" name="sw" %s(PCK("sw"))> \ |
| 883 | + @ Wiki</label><br> |
| 884 | + @ <label><input type="checkbox" name="di" %s(PCK("di"))> \ |
| 885 | + @ Daily digest only</label><br> |
| 886 | + if( g.perm.Admin ){ |
| 887 | + @ <label><input type="checkbox" name="vi" %s(PCK("vi"))> \ |
| 888 | + @ Verified</label><br> |
| 889 | + @ <label><input type="checkbox" name="dnc" %s(PCK("dnc"))> \ |
| 890 | + @ Do not call</label><br> |
| 891 | + } |
| 892 | + @ </td> |
| 511 | 893 | @ </tr> |
| 512 | 894 | @ <tr> |
| 513 | 895 | @ <td></td> |
| 514 | | - @ <td><input type="submit" value="Submit"></td> |
| 896 | + if( needCaptcha && !email_enabled() ){ |
| 897 | + @ <td><input type="submit" name="submit" value="Submit" disabled> |
| 898 | + @ (Email current disabled)</td> |
| 899 | + }else{ |
| 900 | + @ <td><input type="submit" name="submit" value="Submit"></td> |
| 901 | + } |
| 515 | 902 | @ </tr> |
| 516 | 903 | @ </table> |
| 517 | 904 | if( needCaptcha ){ |
| 518 | 905 | @ <div class="captcha"><table class="captcha"><tr><td><pre> |
| 519 | 906 | @ %h(zCaptcha) |
| | @@ -520,7 +907,701 @@ |
| 520 | 907 | @ </pre> |
| 521 | 908 | @ Enter the 8 characters above in the "Security Code" box |
| 522 | 909 | @ </td></tr></table></div> |
| 523 | 910 | } |
| 524 | 911 | @ </form> |
| 912 | + fossil_free(zErr); |
| 913 | + style_footer(); |
| 914 | +} |
| 915 | + |
| 916 | +/* |
| 917 | +** Either shutdown or completely delete a subscription entry given |
| 918 | +** by the hex value zName. Then paint a webpage that explains that |
| 919 | +** the entry has been removed. |
| 920 | +*/ |
| 921 | +static void email_unsubscribe(const char *zName){ |
| 922 | + char *zEmail; |
| 923 | + zEmail = db_text(0, "SELECT semail FROM subscriber" |
| 924 | + " WHERE subscriberCode=hextoblob(%Q)", zName); |
| 925 | + if( zEmail==0 ){ |
| 926 | + style_header("Unsubscribe Fail"); |
| 927 | + @ <p>Unable to locate a subscriber with the requested key</p> |
| 928 | + }else{ |
| 929 | + db_multi_exec( |
| 930 | + "DELETE FROM subscriber WHERE subscriberCode=hextoblob(%Q)", |
| 931 | + zName |
| 932 | + ); |
| 933 | + style_header("Unsubscribed"); |
| 934 | + @ <p>The "%h(zEmail)" email address has been delisted. |
| 935 | + @ All traces of that email address have been removed</p> |
| 936 | + } |
| 937 | + style_footer(); |
| 938 | + return; |
| 939 | +} |
| 940 | + |
| 941 | +/* |
| 942 | +** WEBPAGE: alerts |
| 943 | +** |
| 944 | +** Edit email alert and notification settings. |
| 945 | +** |
| 946 | +** The subscriber entry is identified in either of two ways: |
| 947 | +** |
| 948 | +** (1) The name= query parameter contains the subscriberCode. |
| 949 | +** |
| 950 | +** (2) The user is logged into an account other than "nobody" or |
| 951 | +** "anonymous". In that case the notification settings |
| 952 | +** associated with that account can be edited without needing |
| 953 | +** to know the subscriber code. |
| 954 | +*/ |
| 955 | +void alerts_page(void){ |
| 956 | + const char *zName = P("name"); |
| 957 | + Stmt q; |
| 958 | + int sa, sc, st, sw; |
| 959 | + int sdigest, sdonotcall, sverified; |
| 960 | + const char *ssub; |
| 961 | + const char *semail; |
| 962 | + const char *smip; |
| 963 | + const char *suname; |
| 964 | + int eErr = 0; |
| 965 | + char *zErr = 0; |
| 966 | + |
| 967 | + login_check_credentials(); |
| 968 | + if( !g.perm.EmailAlert ){ |
| 969 | + cgi_redirect("subscribe"); |
| 970 | + return; |
| 971 | + } |
| 972 | + if( zName==0 && login_is_individual() ){ |
| 973 | + zName = db_text(0, "SELECT hex(subscriberCode) FROM subscriber" |
| 974 | + " WHERE suname=%Q", g.zLogin); |
| 975 | + } |
| 976 | + if( zName==0 || !validate16(zName, -1) ){ |
| 977 | + cgi_redirect("subscribe"); |
| 978 | + return; |
| 979 | + } |
| 980 | + email_submenu_common(); |
| 981 | + if( P("submit")!=0 && cgi_csrf_safe(1) ){ |
| 982 | + int sdonotcall = PB("sdonotcall"); |
| 983 | + int sdigest = PB("sdigest"); |
| 984 | + char ssub[10]; |
| 985 | + int nsub = 0; |
| 986 | + if( PB("sa") ) ssub[nsub++] = 'a'; |
| 987 | + if( PB("sc") ) ssub[nsub++] = 'c'; |
| 988 | + if( PB("st") ) ssub[nsub++] = 't'; |
| 989 | + if( PB("sw") ) ssub[nsub++] = 'w'; |
| 990 | + ssub[nsub] = 0; |
| 991 | + if( g.perm.Admin ){ |
| 992 | + const char *suname = PT("suname"); |
| 993 | + if( suname && suname[0]==0 ) suname = 0; |
| 994 | + int sverified = PB("sverified"); |
| 995 | + db_multi_exec( |
| 996 | + "UPDATE subscriber SET" |
| 997 | + " sdonotcall=%d," |
| 998 | + " sdigest=%d," |
| 999 | + " ssub=%Q," |
| 1000 | + " smtime=julianday('now')," |
| 1001 | + " smip=%Q," |
| 1002 | + " suname=%Q," |
| 1003 | + " sverified=%d" |
| 1004 | + " WHERE subscriberCode=hextoblob(%Q)", |
| 1005 | + sdonotcall, |
| 1006 | + sdigest, |
| 1007 | + ssub, |
| 1008 | + g.zIpAddr, |
| 1009 | + suname, |
| 1010 | + sverified, |
| 1011 | + zName |
| 1012 | + ); |
| 1013 | + }else{ |
| 1014 | + db_multi_exec( |
| 1015 | + "UPDATE subscriber SET" |
| 1016 | + " sdonotcall=%d," |
| 1017 | + " sdigest=%d," |
| 1018 | + " ssub=%Q," |
| 1019 | + " smtime=julianday('now')," |
| 1020 | + " smip=%Q," |
| 1021 | + " WHERE subscriberCode=hextoblob(%Q)", |
| 1022 | + sdonotcall, |
| 1023 | + sdigest, |
| 1024 | + ssub, |
| 1025 | + g.zIpAddr, |
| 1026 | + zName |
| 1027 | + ); |
| 1028 | + } |
| 1029 | + } |
| 1030 | + if( P("delete")!=0 && cgi_csrf_safe(1) ){ |
| 1031 | + if( !PB("dodelete") ){ |
| 1032 | + eErr = 9; |
| 1033 | + zErr = mprintf("Select this checkbox and press \"Unsubscribe\" to" |
| 1034 | + " unsubscribe"); |
| 1035 | + }else{ |
| 1036 | + email_unsubscribe(zName); |
| 1037 | + return; |
| 1038 | + } |
| 1039 | + } |
| 1040 | + db_prepare(&q, |
| 1041 | + "SELECT" |
| 1042 | + " semail," |
| 1043 | + " sverified," |
| 1044 | + " sdonotcall," |
| 1045 | + " sdigest," |
| 1046 | + " ssub," |
| 1047 | + " smip," |
| 1048 | + " suname" |
| 1049 | + " FROM subscriber WHERE subscriberCode=hextoblob(%Q)", zName); |
| 1050 | + if( db_step(&q)!=SQLITE_ROW ){ |
| 1051 | + db_finalize(&q); |
| 1052 | + cgi_redirect("subscribe"); |
| 1053 | + return; |
| 1054 | + } |
| 1055 | + style_header("Update Subscription"); |
| 1056 | + semail = db_column_text(&q, 0); |
| 1057 | + sverified = db_column_int(&q, 1); |
| 1058 | + sdonotcall = db_column_int(&q, 2); |
| 1059 | + sdigest = db_column_int(&q, 3); |
| 1060 | + ssub = db_column_text(&q, 4); |
| 1061 | + sa = strchr(ssub,'a')!=0; |
| 1062 | + sc = strchr(ssub,'c')!=0; |
| 1063 | + st = strchr(ssub,'t')!=0; |
| 1064 | + sw = strchr(ssub,'w')!=0; |
| 1065 | + smip = db_column_text(&q, 5); |
| 1066 | + suname = db_column_text(&q, 6); |
| 1067 | + if( !g.perm.Admin && !sverified ){ |
| 1068 | + db_multi_exec( |
| 1069 | + "UPDATE subscriber SET sverified=1 WHERE subscriberCode=hextoblob(%Q)", |
| 1070 | + zName); |
| 1071 | + @ <h1>Your email alert subscription has been verified!</h1> |
| 1072 | + @ <p>Use the form below to update your subscription information.</p> |
| 1073 | + @ <p>Hint: Bookmark this page so that you can more easily update |
| 1074 | + @ your subscription information in the future</p> |
| 1075 | + }else{ |
| 1076 | + @ <p>Make changes to the email subscription shown below and |
| 1077 | + @ press "Submit".</p> |
| 1078 | + } |
| 1079 | + form_begin(0, "%R/alerts"); |
| 1080 | + @ <input type="hidden" name="name" value="%h(zName)"> |
| 1081 | + @ <table class="subscribe"> |
| 1082 | + @ <tr> |
| 1083 | + @ <td class="form_label">Email Address:</td> |
| 1084 | + @ <td>%h(semail)</td> |
| 1085 | + @ </tr> |
| 1086 | + if( g.perm.Admin ){ |
| 1087 | + @ <tr> |
| 1088 | + @ <td class='form_label'>IP Address:</td> |
| 1089 | + @ <td>%h(smip)</td> |
| 1090 | + @ </tr> |
| 1091 | + @ <tr> |
| 1092 | + @ <td class="form_label">User:</td> |
| 1093 | + @ <td><input type="text" name="suname" value="%h(suname?suname:"")" \ |
| 1094 | + @ size="30"></td> |
| 1095 | + @ </tr> |
| 1096 | + } |
| 1097 | + @ <tr> |
| 1098 | + @ <td class="form_label">Options:</td> |
| 1099 | + @ <td><label><input type="checkbox" name="sa" %s(sa?"checked":"")>\ |
| 1100 | + @ Announcements</label><br> |
| 1101 | + @ <label><input type="checkbox" name="sc" %s(sc?"checked":"")>\ |
| 1102 | + @ Check-ins</label><br> |
| 1103 | + @ <label><input type="checkbox" name="st" %s(st?"checked":"")>\ |
| 1104 | + @ Ticket changes</label><br> |
| 1105 | + @ <label><input type="checkbox" name="sw" %s(sw?"checked":"")>\ |
| 1106 | + @ Wiki</label><br> |
| 1107 | + @ <label><input type="checkbox" name="sdigest" %s(sdigest?"checked":"")>\ |
| 1108 | + @ Daily digest only</label><br> |
| 1109 | + if( g.perm.Admin ){ |
| 1110 | + @ <label><input type="checkbox" name="sdonotcall" \ |
| 1111 | + @ %s(sdonotcall?"checked":"")> Do not call</label><br> |
| 1112 | + @ <label><input type="checkbox" name="sverified" \ |
| 1113 | + @ %s(sverified?"checked":"")>\ |
| 1114 | + @ Verified</label><br> |
| 1115 | + } |
| 1116 | + @ <label><input type="checkbox" name="dodelete"> |
| 1117 | + @ Unsubscribe</label> \ |
| 1118 | + if( eErr==9 ){ |
| 1119 | + @ <span class="loginError">← %h(zErr)</span>\ |
| 1120 | + } |
| 1121 | + @ <br> |
| 1122 | + @ </td></tr> |
| 1123 | + @ <tr> |
| 1124 | + @ <td></td> |
| 1125 | + @ <td><input type="submit" name="submit" value="Submit"> |
| 1126 | + @ <input type="submit" name="delete" value="Unsubscribe"> |
| 1127 | + @ </tr> |
| 1128 | + @ </table> |
| 1129 | + @ </form> |
| 1130 | + fossil_free(zErr); |
| 1131 | + db_finalize(&q); |
| 1132 | + style_footer(); |
| 1133 | +} |
| 1134 | + |
| 1135 | +/* This is the message that gets sent to describe how to change |
| 1136 | +** or modify a subscription |
| 1137 | +*/ |
| 1138 | +static const char zUnsubMsg[] = |
| 1139 | +@ To changes your subscription settings at %s visit this link: |
| 1140 | +@ |
| 1141 | +@ %s/alerts/%s |
| 1142 | +@ |
| 1143 | +@ To completely unsubscribe from %s, visit the following link: |
| 1144 | +@ |
| 1145 | +@ %s/unsubscribe/%s |
| 1146 | +; |
| 1147 | + |
| 1148 | +/* |
| 1149 | +** WEBPAGE: unsubscribe |
| 1150 | +** |
| 1151 | +** Users visit this page to be delisted from email alerts. |
| 1152 | +** |
| 1153 | +** If a valid subscriber code is supplied in the name= query parameter, |
| 1154 | +** then that subscriber is delisted. |
| 1155 | +** |
| 1156 | +** Otherwise, If the users is logged in, then they are redirected |
| 1157 | +** to the /alerts page where they have an unsubscribe button. |
| 1158 | +** |
| 1159 | +** Non-logged-in users with no name= query parameter are invited to enter |
| 1160 | +** an email address to which will be sent the unsubscribe link that |
| 1161 | +** contains the correct subscriber code. |
| 1162 | +*/ |
| 1163 | +void unsubscribe_page(void){ |
| 1164 | + const char *zName = P("name"); |
| 1165 | + char *zErr = 0; |
| 1166 | + int eErr = 0; |
| 1167 | + unsigned int uSeed; |
| 1168 | + const char *zDecoded; |
| 1169 | + char *zCaptcha = 0; |
| 1170 | + int dx; |
| 1171 | + int bSubmit; |
| 1172 | + const char *zEAddr; |
| 1173 | + char *zCode = 0; |
| 1174 | + |
| 1175 | + /* If a valid subscriber code is supplied, then unsubscribe immediately. |
| 1176 | + */ |
| 1177 | + if( zName |
| 1178 | + && db_exists("SELECT 1 FROM subscriber WHERE subscriberCode=hextoblob(%Q)", |
| 1179 | + zName) |
| 1180 | + ){ |
| 1181 | + email_unsubscribe(zName); |
| 1182 | + return; |
| 1183 | + } |
| 1184 | + |
| 1185 | + /* Logged in users are redirected to the /alerts page */ |
| 1186 | + login_check_credentials(); |
| 1187 | + if( login_is_individual() ){ |
| 1188 | + cgi_redirectf("%R/alerts"); |
| 1189 | + return; |
| 1190 | + } |
| 1191 | + |
| 1192 | + zEAddr = PD("e",""); |
| 1193 | + dx = atoi(PD("dx","0")); |
| 1194 | + bSubmit = P("submit")!=0 && P("e")!=0 && cgi_csrf_safe(1); |
| 1195 | + if( bSubmit ){ |
| 1196 | + if( !captcha_is_correct(1) ){ |
| 1197 | + eErr = 2; |
| 1198 | + zErr = mprintf("enter the security code shown below"); |
| 1199 | + bSubmit = 0; |
| 1200 | + } |
| 1201 | + } |
| 1202 | + if( bSubmit ){ |
| 1203 | + zCode = db_text(0,"SELECT hex(subscriberCode) FROM subscriber" |
| 1204 | + " WHERE semail=%Q", zEAddr); |
| 1205 | + if( zCode==0 ){ |
| 1206 | + eErr = 1; |
| 1207 | + zErr = mprintf("not a valid email address"); |
| 1208 | + bSubmit = 0; |
| 1209 | + } |
| 1210 | + } |
| 1211 | + if( bSubmit ){ |
| 1212 | + /* If we get this far, it means that a valid unsubscribe request has |
| 1213 | + ** been submitted. Send the appropriate email. */ |
| 1214 | + Blob hdr, body; |
| 1215 | + blob_init(&hdr,0,0); |
| 1216 | + blob_init(&body,0,0); |
| 1217 | + blob_appendf(&hdr, "To: %s\n", zEAddr); |
| 1218 | + blob_appendf(&hdr, "Subject: Unsubscribe Instructions\n"); |
| 1219 | + blob_appendf(&body, zUnsubMsg/*works-like:"%s%s%s%s%s%s"*/, |
| 1220 | + g.zBaseURL, g.zBaseURL, zCode, g.zBaseURL, g.zBaseURL, zCode); |
| 1221 | + email_send(&hdr, &body, 0, 0); |
| 1222 | + style_header("Unsubscribe Instructions Sent"); |
| 1223 | + @ <p>An email has been sent to "%h(zEAddr)" that explains how to |
| 1224 | + @ unsubscribe and/or modify your subscription settings</p> |
| 1225 | + style_footer(); |
| 1226 | + return; |
| 1227 | + } |
| 1228 | + |
| 1229 | + /* Non-logged-in users have to enter an email address to which is |
| 1230 | + ** sent a message containing the unsubscribe link. |
| 1231 | + */ |
| 1232 | + style_header("Unsubscribe Request"); |
| 1233 | + @ <p>Fill out the form below to request an email message that will |
| 1234 | + @ explain how to unsubscribe and/or change your subscription settings.</p> |
| 1235 | + @ |
| 1236 | + form_begin(0, "%R/unsubscribe"); |
| 1237 | + @ <table class="subscribe"> |
| 1238 | + @ <tr> |
| 1239 | + @ <td class="form_label">Email Address:</td> |
| 1240 | + @ <td><input type="text" name="e" value="%h(zEAddr)" size="30"></td> |
| 1241 | + if( eErr==1 ){ |
| 1242 | + @ <td><span class="loginError">← %h(zErr)</span></td> |
| 1243 | + } |
| 1244 | + @ </tr> |
| 1245 | + uSeed = captcha_seed(); |
| 1246 | + zDecoded = captcha_decode(uSeed); |
| 1247 | + zCaptcha = captcha_render(zDecoded); |
| 1248 | + @ <tr> |
| 1249 | + @ <td class="form_label">Security Code:</td> |
| 1250 | + @ <td><input type="text" name="captcha" value="" size="30"> |
| 1251 | + @ <input type="hidden" name="captchaseed" value="%u(uSeed)"></td> |
| 1252 | + if( eErr==2 ){ |
| 1253 | + @ <td><span class="loginError">← %h(zErr)</span></td> |
| 1254 | + } |
| 1255 | + @ </tr> |
| 1256 | + @ <tr> |
| 1257 | + @ <td class="form_label">Options:</td> |
| 1258 | + @ <td><label><input type="radio" name="dx" value="0" %s(dx?"":"checked")>\ |
| 1259 | + @ Modify subscription</label><br> |
| 1260 | + @ <label><input type="radio" name="dx" value="1" %s(dx?"checked":"")>\ |
| 1261 | + @ Completely unsubscribe</label><br> |
| 1262 | + @ <tr> |
| 1263 | + @ <td></td> |
| 1264 | + @ <td><input type="submit" name="submit" value="Submit"></td> |
| 1265 | + @ </tr> |
| 1266 | + @ </table> |
| 1267 | + @ <div class="captcha"><table class="captcha"><tr><td><pre> |
| 1268 | + @ %h(zCaptcha) |
| 1269 | + @ </pre> |
| 1270 | + @ Enter the 8 characters above in the "Security Code" box |
| 1271 | + @ </td></tr></table></div> |
| 1272 | + @ </form> |
| 1273 | + fossil_free(zErr); |
| 1274 | + style_footer(); |
| 1275 | +} |
| 1276 | + |
| 1277 | +/* |
| 1278 | +** WEBPAGE: subscribers |
| 1279 | +** |
| 1280 | +** This page, accessible to administrators only, |
| 1281 | +** shows a list of email notification email addresses with |
| 1282 | +** links to facilities for editing. |
| 1283 | +*/ |
| 1284 | +void subscriber_list_page(void){ |
| 1285 | + Blob sql; |
| 1286 | + Stmt q; |
| 1287 | + login_check_credentials(); |
| 1288 | + if( !g.perm.Admin ){ |
| 1289 | + fossil_redirect_home(); |
| 1290 | + return; |
| 1291 | + } |
| 1292 | + email_submenu_common(); |
| 1293 | + style_header("Subscriber List"); |
| 1294 | + blob_init(&sql, 0, 0); |
| 1295 | + blob_append_sql(&sql, |
| 1296 | + "SELECT hex(subscriberCode)," |
| 1297 | + " semail," |
| 1298 | + " ssub," |
| 1299 | + " suname," |
| 1300 | + " sverified," |
| 1301 | + " sdigest" |
| 1302 | + " FROM subscriber" |
| 1303 | + ); |
| 1304 | + db_prepare_blob(&q, &sql); |
| 1305 | + @ <table border="1"> |
| 1306 | + @ <tr> |
| 1307 | + @ <th>Email |
| 1308 | + @ <th>Events |
| 1309 | + @ <th>Digest-Only? |
| 1310 | + @ <th>User |
| 1311 | + @ <th>Verified? |
| 1312 | + @ </tr> |
| 1313 | + while( db_step(&q)==SQLITE_ROW ){ |
| 1314 | + @ <tr> |
| 1315 | + @ <td><a href='%R/alerts/%s(db_column_text(&q,0))'>\ |
| 1316 | + @ %h(db_column_text(&q,1))</a></td> |
| 1317 | + @ <td>%h(db_column_text(&q,2))</td> |
| 1318 | + @ <td>%s(db_column_int(&q,5)?"digest":"")</td> |
| 1319 | + @ <td>%h(db_column_text(&q,3))</td> |
| 1320 | + @ <td>%s(db_column_int(&q,4)?"yes":"pending")</td> |
| 1321 | + @ </tr> |
| 1322 | + } |
| 1323 | + @ </table> |
| 1324 | + db_finalize(&q); |
| 525 | 1325 | style_footer(); |
| 526 | 1326 | } |
| 1327 | + |
| 1328 | +#if LOCAL_INTERFACE |
| 1329 | +/* |
| 1330 | +** A single event that might appear in an alert is recorded as an |
| 1331 | +** instance of the following object. |
| 1332 | +*/ |
| 1333 | +struct EmailEvent { |
| 1334 | + int type; /* 'c', 't', 'w', etc. */ |
| 1335 | + Blob txt; /* Text description to appear in an alert */ |
| 1336 | + EmailEvent *pNext; /* Next in chronological order */ |
| 1337 | +}; |
| 1338 | +#endif |
| 1339 | + |
| 1340 | +/* |
| 1341 | +** Free a linked list of EmailEvent objects |
| 1342 | +*/ |
| 1343 | +void email_free_eventlist(EmailEvent *p){ |
| 1344 | + while( p ){ |
| 1345 | + EmailEvent *pNext = p->pNext; |
| 1346 | + blob_zero(&p->txt); |
| 1347 | + fossil_free(p); |
| 1348 | + p = pNext; |
| 1349 | + } |
| 1350 | +} |
| 1351 | + |
| 1352 | +/* |
| 1353 | +** Compute and return a linked list of EmailEvent objects |
| 1354 | +** corresponding to the current content of the temp.wantalert |
| 1355 | +** table which should be defined as follows: |
| 1356 | +** |
| 1357 | +** CREATE TEMP TABLE wantalert(eventId TEXT); |
| 1358 | +*/ |
| 1359 | +EmailEvent *email_compute_event_text(int *pnEvent){ |
| 1360 | + Stmt q; |
| 1361 | + EmailEvent *p; |
| 1362 | + EmailEvent anchor; |
| 1363 | + EmailEvent *pLast; |
| 1364 | + const char *zUrl = db_get("email-url","http://localhost:8080"); |
| 1365 | + |
| 1366 | + db_prepare(&q, |
| 1367 | + "SELECT" |
| 1368 | + " blob.uuid," /* 0 */ |
| 1369 | + " datetime(event.mtime)," /* 1 */ |
| 1370 | + " coalesce(ecomment,comment)" |
| 1371 | + " || ' (user: ' || coalesce(euser,user,'?')" |
| 1372 | + " || (SELECT case when length(x)>0 then ' tags: ' || x else '' end" |
| 1373 | + " FROM (SELECT group_concat(substr(tagname,5), ', ') AS x" |
| 1374 | + " FROM tag, tagxref" |
| 1375 | + " WHERE tagname GLOB 'sym-*' AND tag.tagid=tagxref.tagid" |
| 1376 | + " AND tagxref.rid=blob.rid AND tagxref.tagtype>0))" |
| 1377 | + " || ')' as comment," /* 2 */ |
| 1378 | + " tagxref.value AS branch," /* 3 */ |
| 1379 | + " wantalert.eventId" /* 4 */ |
| 1380 | + " FROM temp.wantalert JOIN tag CROSS JOIN event CROSS JOIN blob" |
| 1381 | + " LEFT JOIN tagxref ON tagxref.tagid=tag.tagid" |
| 1382 | + " AND tagxref.tagtype>0" |
| 1383 | + " AND tagxref.rid=blob.rid" |
| 1384 | + " WHERE blob.rid=event.objid" |
| 1385 | + " AND tag.tagname='branch'" |
| 1386 | + " AND event.objid=substr(wantalert.eventId,2)+0" |
| 1387 | + " ORDER BY event.mtime" |
| 1388 | + ); |
| 1389 | + memset(&anchor, 0, sizeof(anchor)); |
| 1390 | + pLast = &anchor; |
| 1391 | + *pnEvent = 0; |
| 1392 | + while( db_step(&q)==SQLITE_ROW ){ |
| 1393 | + const char *zType = ""; |
| 1394 | + p = fossil_malloc( sizeof(EmailEvent) ); |
| 1395 | + pLast->pNext = p; |
| 1396 | + pLast = p; |
| 1397 | + p->type = db_column_text(&q, 4)[0]; |
| 1398 | + p->pNext = 0; |
| 1399 | + switch( p->type ){ |
| 1400 | + case 'c': zType = "Check-In"; break; |
| 1401 | + case 't': zType = "Wiki Edit"; break; |
| 1402 | + case 'w': zType = "Ticket Change"; break; |
| 1403 | + } |
| 1404 | + blob_init(&p->txt, 0, 0); |
| 1405 | + blob_appendf(&p->txt,"== %s %s ==\n%s\n%s/info/%.20s\n", |
| 1406 | + db_column_text(&q,1), |
| 1407 | + zType, |
| 1408 | + db_column_text(&q,2), |
| 1409 | + zUrl, |
| 1410 | + db_column_text(&q,0) |
| 1411 | + ); |
| 1412 | + (*pnEvent)++; |
| 1413 | + } |
| 1414 | + db_finalize(&q); |
| 1415 | + return anchor.pNext; |
| 1416 | +} |
| 1417 | + |
| 1418 | +/* |
| 1419 | +** Put a header on an alert email |
| 1420 | +*/ |
| 1421 | +void email_header(Blob *pOut){ |
| 1422 | + blob_appendf(pOut, |
| 1423 | + "This is an automated email reporting changes " |
| 1424 | + "on Fossil repository %s (%s/timeline)\n", |
| 1425 | + db_get("email-subname","(unknown)"), |
| 1426 | + db_get("email-url","http://localhost:8080")); |
| 1427 | +} |
| 1428 | + |
| 1429 | +/* |
| 1430 | +** Append the "unsubscribe" notification and other footer text to |
| 1431 | +** the end of an email alert being assemblied in pOut. |
| 1432 | +*/ |
| 1433 | +void email_footer(Blob *pOut){ |
| 1434 | + blob_appendf(pOut, "\n%.72c\nTo unsubscribe: %s/unsubscribe\n", |
| 1435 | + '-', db_get("email-url","http://localhost:8080")); |
| 1436 | +} |
| 1437 | + |
| 1438 | +/* |
| 1439 | +** COMMAND: test-generate-alert |
| 1440 | +** |
| 1441 | +** Usage: %fossil test-generate-alert [--html] [--actual] EVENTID ... |
| 1442 | +** |
| 1443 | +** Generate the text of an email alert for all of the EVENTIDs |
| 1444 | +** listed on the command-line. Write that text to standard |
| 1445 | +** output. If the --actual flag is present, then the EVENTIDs are |
| 1446 | +** the actual event-ids in the pending_alert table. |
| 1447 | +** |
| 1448 | +** This command is intended for testing and debugging the logic |
| 1449 | +** that generates email alert text. |
| 1450 | +*/ |
| 1451 | +void test_generate_alert_cmd(void){ |
| 1452 | + int bActual = find_option("actual",0,0)!=0; |
| 1453 | + Blob out; |
| 1454 | + int nEvent; |
| 1455 | + EmailEvent *pEvent, *p; |
| 1456 | + |
| 1457 | + db_find_and_open_repository(0, 0); |
| 1458 | + verify_all_options(); |
| 1459 | + db_begin_transaction(); |
| 1460 | + email_schema(); |
| 1461 | + db_multi_exec("CREATE TEMP TABLE wantalert(eventid TEXT)"); |
| 1462 | + if( bActual ){ |
| 1463 | + db_multi_exec("INSERT INTO wantalert SELECT eventid FROM pending_alert"); |
| 1464 | + }else{ |
| 1465 | + int i; |
| 1466 | + for(i=2; i<g.argc; i++){ |
| 1467 | + db_multi_exec("INSERT INTO wantalert VALUES(%Q)", g.argv[i]); |
| 1468 | + } |
| 1469 | + } |
| 1470 | + blob_init(&out, 0, 0); |
| 1471 | + email_header(&out); |
| 1472 | + pEvent = email_compute_event_text(&nEvent); |
| 1473 | + for(p=pEvent; p; p=p->pNext){ |
| 1474 | + blob_append(&out, "\n", 1); |
| 1475 | + blob_append(&out, blob_buffer(&p->txt), blob_size(&p->txt)); |
| 1476 | + } |
| 1477 | + email_free_eventlist(pEvent); |
| 1478 | + email_footer(&out); |
| 1479 | + fossil_print("%s", blob_str(&out)); |
| 1480 | + blob_zero(&out); |
| 1481 | + db_end_transaction(0); |
| 1482 | +} |
| 1483 | + |
| 1484 | +/* |
| 1485 | +** COMMAND: test-add-alerts |
| 1486 | +** |
| 1487 | +** Usage: %fossil test-add-alerts EVENTID ... |
| 1488 | +** |
| 1489 | +** Add one or more events to the pending_alert queue. Use this |
| 1490 | +** command during testing to force email notifications for specific |
| 1491 | +** events. |
| 1492 | +*/ |
| 1493 | +void test_add_alert_cmd(void){ |
| 1494 | + int i; |
| 1495 | + db_find_and_open_repository(0, 0); |
| 1496 | + verify_all_options(); |
| 1497 | + db_begin_transaction(); |
| 1498 | + email_schema(); |
| 1499 | + for(i=2; i<g.argc; i++){ |
| 1500 | + db_multi_exec("INSERT INTO pending_alert(eventId) VALUES(%Q)", g.argv[i]); |
| 1501 | + } |
| 1502 | + db_end_transaction(0); |
| 1503 | +} |
| 1504 | + |
| 1505 | +#if INTERFACE |
| 1506 | +/* |
| 1507 | +** Flags for email_send_alerts() |
| 1508 | +*/ |
| 1509 | +#define SENDALERT_DIGEST 0x0001 /* Send a digest */ |
| 1510 | +#define SENDALERT_PRESERVE 0x0002 /* Do not mark the task as done */ |
| 1511 | + |
| 1512 | +#endif /* INTERFACE */ |
| 1513 | + |
| 1514 | +/* |
| 1515 | +** Send alert emails to all subscribers |
| 1516 | +*/ |
| 1517 | +void email_send_alerts(u32 flags){ |
| 1518 | + EmailEvent *pEvents, *p; |
| 1519 | + int nEvent = 0; |
| 1520 | + Stmt q; |
| 1521 | + const char *zDigest = "false"; |
| 1522 | + Blob hdr, body; |
| 1523 | + const char *zUrl; |
| 1524 | + const char *zRepoName; |
| 1525 | + const char *zFrom; |
| 1526 | + |
| 1527 | + db_begin_transaction(); |
| 1528 | + if( !email_enabled() ) goto send_alerts_done; |
| 1529 | + zUrl = db_get("email-url",0); |
| 1530 | + if( zUrl==0 ) goto send_alerts_done; |
| 1531 | + zRepoName = db_get("email-subname",0); |
| 1532 | + if( zRepoName==0 ) goto send_alerts_done; |
| 1533 | + zFrom = db_get("email-self",0); |
| 1534 | + if( zFrom==0 ) goto send_alerts_done; |
| 1535 | + db_multi_exec( |
| 1536 | + "DROP TABLE IF EXISTS temp.wantalert;" |
| 1537 | + "CREATE TEMP TABLE wantalert(eventId TEXT);" |
| 1538 | + ); |
| 1539 | + if( flags & SENDALERT_DIGEST ){ |
| 1540 | + db_multi_exec( |
| 1541 | + "INSERT INTO wantalert SELECT eventid FROM pending_alert" |
| 1542 | + " WHERE sentDigest IS FALSE" |
| 1543 | + ); |
| 1544 | + zDigest = "true"; |
| 1545 | + }else{ |
| 1546 | + db_multi_exec( |
| 1547 | + "INSERT INTO wantalert SELECT eventid FROM pending_alert" |
| 1548 | + " WHERE sentSep IS FALSE" |
| 1549 | + ); |
| 1550 | + } |
| 1551 | + pEvents = email_compute_event_text(&nEvent); |
| 1552 | + if( nEvent==0 ) return; |
| 1553 | + blob_init(&hdr, 0, 0); |
| 1554 | + blob_init(&body, 0, 0); |
| 1555 | + db_prepare(&q, |
| 1556 | + "SELECT" |
| 1557 | + " subscriberCode," /* 0 */ |
| 1558 | + " semail," /* 1 */ |
| 1559 | + " ssub" /* 2 */ |
| 1560 | + " FROM subscriber" |
| 1561 | + " WHERE sverified AND NOT sdonotcall" |
| 1562 | + " AND sdigest IS %s", |
| 1563 | + zDigest/*safe-for-%s*/ |
| 1564 | + ); |
| 1565 | + while( db_step(&q)==SQLITE_ROW ){ |
| 1566 | + const char *zCode = db_column_text(&q, 0); |
| 1567 | + const char *zSub = db_column_text(&q, 2); |
| 1568 | + const char *zEmail = db_column_text(&q, 1); |
| 1569 | + int nHit = 0; |
| 1570 | + for(p=pEvents; p; p=p->pNext){ |
| 1571 | + if( strchr(zSub,p->type)==0 ) continue; |
| 1572 | + if( nHit==0 ){ |
| 1573 | + blob_appendf(&hdr,"To: %s\n", zEmail); |
| 1574 | + blob_appendf(&hdr,"From: %s\n", zFrom); |
| 1575 | + blob_appendf(&hdr,"Subject: %s activity alert\n", zRepoName); |
| 1576 | + blob_appendf(&body, |
| 1577 | + "This is an automated email sent by the Fossil repository " |
| 1578 | + "at %s to alert you to changes.\n", |
| 1579 | + zUrl |
| 1580 | + ); |
| 1581 | + } |
| 1582 | + nHit++; |
| 1583 | + blob_append(&body, "\n", 1); |
| 1584 | + blob_append(&body, blob_buffer(&p->txt), blob_size(&p->txt)); |
| 1585 | + } |
| 1586 | + if( nHit==0 ) continue; |
| 1587 | + blob_appendf(&body,"\n%.72c\nSubscription info: %s/alerts/%s\n", |
| 1588 | + '-', zUrl, zCode); |
| 1589 | + email_send(&hdr,&body,0,0); |
| 1590 | + blob_truncate(&hdr); |
| 1591 | + blob_truncate(&body); |
| 1592 | + } |
| 1593 | + blob_zero(&hdr); |
| 1594 | + blob_zero(&body); |
| 1595 | + db_finalize(&q); |
| 1596 | + email_free_eventlist(pEvents); |
| 1597 | + if( (flags & SENDALERT_PRESERVE)==0 ){ |
| 1598 | + if( flags & SENDALERT_DIGEST ){ |
| 1599 | + db_multi_exec("UPDATE pending_alert SET sentDigest=true"); |
| 1600 | + }else{ |
| 1601 | + db_multi_exec("UPDATE pending_alert SET sentSep=true"); |
| 1602 | + } |
| 1603 | + db_multi_exec("DELETE FROM pending_alert WHERE sentDigest AND sentSep"); |
| 1604 | + } |
| 1605 | +send_alerts_done: |
| 1606 | + db_end_transaction(0); |
| 1607 | +} |
| 527 | 1608 | |