Fossil

Check-in [b4218987]
Login

Many hyperlinks are disabled.
Use anonymous login to enable hyperlinks.

Overview
Comment:Rework the internal email sending logic so that it is connection-oriented. This makes it more efficient and makes it easier to add support for an SMTP sending method at a later date.
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA3-256:b42189878e66cfd569298fa26a4a73a3face1fd4a3c4ec193ab13b4da408339f
User & Date: drh 2018-06-23 14:24:24
Context
2018-06-23
15:48
Add an initial draft of design notes for the email notification system. check-in: aeb98be8 user: drh tags: trunk
14:24
Rework the internal email sending logic so that it is connection-oriented. This makes it more efficient and makes it easier to add support for an SMTP sending method at a later date. check-in: b4218987 user: drh tags: trunk
01:33
Futher corrections to the email alert trigger. check-in: 2c3b9030 user: drh tags: trunk
Changes
Hide Diffs Unified Diffs Ignore Whitespace Patch

Changes to src/email.c.

283
284
285
286
287
288
289

290
291
292










































































293
294
295
296
297






298




















































299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321

322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376

377
378
379
380




381
382
383
384
385
386
387
388


389
390
391
392

393
394
395
396
397
398
399
400
401
402
403
...
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
...
554
555
556
557
558
559
560
561
562
563
564
565
566

567
568
569
570
571
572
573
...
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
...
820
821
822
823
824
825
826

827
828
829
830
831
832
833
834








835
836
837


838
839
840
841
842
843
844
....
1211
1212
1213
1214
1215
1216
1217

1218
1219
1220
1221
1222
1223
1224
1225








1226
1227


1228
1229
1230
1231
1232
1233
1234
....
1523
1524
1525
1526
1527
1528
1529

1530
1531
1532
1533
1534
1535
1536
1537
1538

1539
1540
1541
1542
1543
1544
1545
....
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
....
1602
1603
1604
1605
1606
1607
1608

1609
1610
#if defined(_WIN32) || defined(WIN32)
# undef popen
# define popen _popen
# undef pclose
# define pclose _pclose
#endif


/*
** Send an email message using whatever sending mechanism is configured
** by these settings:










































































**
**   email-send-method    "off"   Do not send any emails
**                        "pipe"  Pipe the email to email-send-command
**                        "db"    Store the mail in database email-send-db
**                        "file"  Store the email as a file in email-send-dir 






**




















































** The recepient(s) must be specified using  "To:" or "Cc:" or "Bcc:" fields
** in the header.  Likewise, the header must contains a "Subject:" line.
** The header might also include fields like "Message-Id:" or
** "In-Reply-To:".
**
** This routine will add fields to the header as follows:
**
**     From:
**     Content-Type:
**     Content-Transfer-Encoding:
**     
** At least one body must be supplied.
**
** The caller maintains ownership of the input Blobs.  This routine will
** read the Blobs and send them onward to the email system, but it will
** not free them.
**
** If zDest is not NULL then it is an overwrite for the email-send-method.
** zDest can be "stdout" to send output to the console for debugging.
*/
void email_send(Blob *pHdr, Blob *pPlain, Blob *pHtml, const char *zDest){
  const char *zFrom = db_get("email-self", 0);
  char *zBoundary = 0;

  Blob all;
  if( zFrom==0 ){
    fossil_warning("Missing configuration: \"email-self\"");
    return;
  }
  if( zDest==0 ) zDest = db_get("email-send-method", "off");
  if( strcmp(zDest, "off")==0 ){
    return;
  }
  blob_init(&all, 0, 0);
  blob_append(&all, blob_buffer(pHdr), blob_size(pHdr));
  blob_appendf(&all, "From: %s\r\n", zFrom);
  if( pPlain && pHtml ){
    blob_appendf(&all, "MIME-Version: 1.0\r\n");
    zBoundary = db_text(0, "SELECT hex(randomblob(20))");
    blob_appendf(&all, "Content-Type: multipart/alternative;"
                       " boundary=\"%s\"\r\n", zBoundary);
  }
  if( pPlain ){
    blob_add_final_newline(pPlain);
    if( zBoundary ){
      blob_appendf(&all, "\r\n--%s\r\n", zBoundary);
    }
    blob_appendf(&all,"Content-Type: text/plain\r\n");
    blob_appendf(&all, "Content-Transfer-Encoding: base64\r\n\r\n");
    append_base64(&all, pPlain);
  }
  if( pHtml ){
    blob_add_final_newline(pHtml);
    if( zBoundary ){
      blob_appendf(&all, "--%s\r\n", zBoundary);
    }
    blob_appendf(&all,"Content-Type: text/html\r\n");
    blob_appendf(&all, "Content-Transfer-Encoding: base64\r\n\r\n");
    append_base64(&all, pHtml);
  }
  if( zBoundary ){
    blob_appendf(&all, "--%s--\r\n", zBoundary);
    fossil_free(zBoundary);
    zBoundary = 0;
  }
  if( strcmp(zDest, "db")==0 ){
    sqlite3 *db;
    sqlite3_stmt *pStmt;
    int rc;
    const char *zDb = db_get("email-send-db",0);
    rc = sqlite3_open(zDb, &db);
    if( rc==SQLITE_OK ){
      sqlite3_exec(db, "CREATE TABLE IF NOT EXISTS email(\n"
                       "  emailid INTEGER PRIMARY KEY,\n"
                       "  msg TEXT\n);", 0, 0, 0);
      rc = sqlite3_prepare_v2(db, "INSERT INTO email(msg) VALUES(?1)", -1,
                              &pStmt, 0);
      if( rc==SQLITE_OK ){
        sqlite3_bind_text(pStmt, 1, blob_str(&all), -1, SQLITE_TRANSIENT);

        sqlite3_step(pStmt);
        sqlite3_finalize(pStmt);
      }
      sqlite3_close(db);




    }
  }else if( strcmp(zDest, "pipe")==0 ){
    const char *zCmd = db_get("email-send-command", 0);
    if( zCmd ){
      FILE *out = popen(zCmd, "w");
      if( out ){
        fwrite(blob_buffer(&all), 1, blob_size(&all), out);
        fclose(out);


      }
    }
  }else if( strcmp(zDest, "dir")==0 ){
    const char *zDir = db_get("email-send-dir","./");

    char *zFile = emailTempFilename(zDir);
    blob_write_to_file(&all, zFile);
    fossil_free(zFile);
  }else if( strcmp(zDest, "stdout")==0 ){
    fossil_print("%s\n", blob_str(&all));
  }
  blob_zero(&all);
}

/*
** Analyze and act on a received email.
................................................................................
**
**    send TO [OPTIONS]       Send a single email message using whatever
**                            email sending mechanism is currently configured.
**                            Use this for testing the email configuration.
**                            Options:
**
**                              --body FILENAME
**                              --html
**                              --stdout
**                              --subject|-S SUBJECT
**
**    settings [NAME VALUE]   With no arguments, list all email settings.
**                            Or change the value of a single email setting.
**
**    subscribers [PATTERN]   List all subscribers matching PATTERN.
................................................................................
        "DROP TABLE IF EXISTS subscription;\n"
      );
      email_schema();
    }
  }else
  if( strncmp(zCmd, "send", nCmd)==0 ){
    Blob prompt, body, hdr;
    int sendAsBoth = find_option("both",0,0)!=0;
    int sendAsHtml = find_option("html",0,0)!=0;
    const char *zDest = find_option("stdout",0,0)!=0 ? "stdout" : 0;
    int i;
    const char *zSubject = find_option("subject", "S", 1);
    const char *zSource = find_option("body", 0, 1);

    verify_all_options();
    blob_init(&prompt, 0, 0);
    blob_init(&body, 0, 0);
    blob_init(&hdr, 0, 0);
    for(i=3; i<g.argc; i++){
      blob_appendf(&hdr, "To: %s\n", g.argv[i]);
    }
................................................................................
    }
    if( zSource ){
      blob_read_from_file(&body, zSource, ExtFILE);
    }else{
      prompt_for_user_comment(&body, &prompt);
    }
    blob_add_final_newline(&body);
    if( sendAsHtml ){
      email_send(&hdr, 0, &body, zDest);
    }else if( sendAsBoth ){
      Blob html;
      blob_init(&html, 0, 0);
      blob_appendf(&html, "<pre>\n%h</pre>\n", blob_str(&body));
      email_send(&hdr, &body, &html, zDest);
      blob_zero(&html);
    }else{
      email_send(&hdr, &body, 0, zDest);
    }
    blob_zero(&hdr);
    blob_zero(&body);
    blob_zero(&prompt);
  }else
  if( strncmp(zCmd, "settings", nCmd)==0 ){
    int isGlobal = find_option("global",0,0)!=0;
    int nSetting;
................................................................................
      ** No verification is required.  Jump immediately to /alerts page.
      */
      cgi_redirectf("%R/alerts/%s", zCode);
      return;
    }else{
      /* We need to send a verification email */
      Blob hdr, body;

      blob_init(&hdr,0,0);
      blob_init(&body,0,0);
      blob_appendf(&hdr, "To: %s\n", zEAddr);
      blob_appendf(&hdr, "Subject: Subscription verification\n");
      blob_appendf(&body, zConfirmMsg/*works-like:"%s%s%s"*/,
                   g.zBaseURL, g.zBaseURL, zCode);
      email_send(&hdr, &body, 0, 0);
      style_header("Email Alert Verification");








      @ <p>An email has been sent to "%h(zEAddr)". That email contains a
      @ hyperlink that you must click on in order to activate your
      @ subscription.</p>


      style_footer();
    }
    return;
  }
  style_header("Signup For Email Alerts");
  @ <p>To receive email notifications for changes to this
  @ repository, fill out the form below and press "Submit" button.</p>
................................................................................
      bSubmit = 0;
    }
  }
  if( bSubmit ){
    /* If we get this far, it means that a valid unsubscribe request has
    ** been submitted.  Send the appropriate email. */
    Blob hdr, body;

    blob_init(&hdr,0,0);
    blob_init(&body,0,0);
    blob_appendf(&hdr, "To: %s\n", zEAddr);
    blob_appendf(&hdr, "Subject: Unsubscribe Instructions\n");
    blob_appendf(&body, zUnsubMsg/*works-like:"%s%s%s%s%s%s"*/,
                  g.zBaseURL, g.zBaseURL, zCode, g.zBaseURL, g.zBaseURL, zCode);
    email_send(&hdr, &body, 0, 0);
    style_header("Unsubscribe Instructions Sent");








    @ <p>An email has been sent to "%h(zEAddr)" that explains how to
    @ unsubscribe and/or modify your subscription settings</p>


    style_footer();
    return;
  }  

  /* Non-logged-in users have to enter an email address to which is
  ** sent a message containing the unsubscribe link.
  */
................................................................................
  Stmt q;
  const char *zDigest = "false";
  Blob hdr, body;
  const char *zUrl;
  const char *zRepoName;
  const char *zFrom;
  const char *zDest = (flags & SENDALERT_STDOUT) ? "stdout" : 0;


  db_begin_transaction();
  if( !email_enabled() ) goto send_alerts_done;
  zUrl = db_get("email-url",0);
  if( zUrl==0 ) goto send_alerts_done;
  zRepoName = db_get("email-subname",0);
  if( zRepoName==0 ) goto send_alerts_done;
  zFrom = db_get("email-self",0);
  if( zFrom==0 ) goto send_alerts_done;

  db_multi_exec(
    "DROP TABLE IF EXISTS temp.wantalert;"
    "CREATE TEMP TABLE wantalert(eventId TEXT);"
  );
  if( flags & SENDALERT_DIGEST ){
    db_multi_exec(
      "INSERT INTO wantalert SELECT eventid FROM pending_alert"
................................................................................
      nHit++;
      blob_append(&body, "\n", 1);
      blob_append(&body, blob_buffer(&p->txt), blob_size(&p->txt));
    }
    if( nHit==0 ) continue;
    blob_appendf(&body,"\n%.72c\nSubscription info: %s/alerts/%s\n",
         '-', zUrl, zCode);
    email_send(&hdr,&body,0,zDest);
    blob_truncate(&hdr);
    blob_truncate(&body);
  }
  blob_zero(&hdr);
  blob_zero(&body);
  db_finalize(&q);
  email_free_eventlist(pEvents);
................................................................................
      db_multi_exec("UPDATE pending_alert SET sentDigest=true");
    }else{
      db_multi_exec("UPDATE pending_alert SET sentSep=true");
    }
    db_multi_exec("DELETE FROM pending_alert WHERE sentDigest AND sentSep");
  }
send_alerts_done:

  db_end_transaction(0);
}







>

<
|
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>

<
<
<
<
>
>
>
>
>
>

>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>











<
<



<
<
<

<
<
<
>

<
<
<
<
<
|




|
<
<
<
<
<
<
<
|
<
<
<
|
|
|
<
|
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
|
<
<
<
<
<
<
<
<
<
|
>
|
<
|
<
>
>
>
>

<
<
|
|
|
|
|
>
>
|
<
<
<
>
|


|







 







<







 







<
<




>







 







|
|
|
<
<
<
<
<
<
<
<







 







>






|

>
>
>
>
>
>
>
>
|
|
|
>
>







 







>






|

>
>
>
>
>
>
>
>
|
|
>
>







 







>









>







 







|







 







>


283
284
285
286
287
288
289
290
291

292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367




368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437


438
439
440



441



442
443





444
445
446
447
448
449







450



451
452
453

454
















455









456
457
458

459

460
461
462
463
464


465
466
467
468
469
470
471
472



473
474
475
476
477
478
479
480
481
482
483
484
...
562
563
564
565
566
567
568

569
570
571
572
573
574
575
...
634
635
636
637
638
639
640


641
642
643
644
645
646
647
648
649
650
651
652
...
655
656
657
658
659
660
661
662
663
664








665
666
667
668
669
670
671
...
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
....
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
....
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
....
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
....
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
#if defined(_WIN32) || defined(WIN32)
# undef popen
# define popen _popen
# undef pclose
# define pclose _pclose
#endif

#if INTERFACE
/*

** An instance of the following object is used to send emails.
*/
struct EmailSender {
  sqlite3 *db;               /* Database emails are sent to */
  sqlite3_stmt *pStmt;       /* Stmt to insert into the database */
  const char *zDest;         /* How to send email. */
  const char *zDb;           /* Name of database file */
  const char *zDir;          /* Directory in which to store as email files */
  const char *zCmd;          /* Command to run for each email */
  const char *zFrom;         /* Emails come from here */
  char *zErr;                /* Error message */
  int bImmediateFail;        /* On any error, call fossil_fatal() */
};
#endif /* INTERFACE */

/*
** Shutdown an emailer.  Clear all information other than the error message.
*/
static void emailerShutdown(EmailSender *p){
  sqlite3_finalize(p->pStmt);
  p->pStmt = 0;
  sqlite3_free(p->db);
  p->db = 0;
  p->zDb = 0;
  p->zDir = 0;
  p->zCmd = 0;
  p->zDest = "off";
}

/*
** Put the EmailSender into an error state.
*/
static void emailerError(EmailSender *p, const char *zFormat, ...){
  va_list ap;
  fossil_free(p->zErr);
  va_start(ap, zFormat);
  p->zErr = vmprintf(zFormat, ap);
  va_end(ap);
  emailerShutdown(p);
  if( p->bImmediateFail ){
    fossil_fatal("%s", p->zErr);
  }
}

/*
** Free an email sender object
*/
void email_sender_free(EmailSender *p){
  emailerShutdown(p);
  fossil_free(p->zErr);
  fossil_free(p);
}

/*
** Get an email setting value.  Report an error if not configured.
** Return 0 on success and one if there is an error.
*/
static int emailerGetSetting(
  EmailSender *p,        /* Where to report the error */
  const char **pzVal,    /* Write the setting value here */
  const char *zName      /* Name of the setting */
){
  const char *z = db_get(zName, 0);
  int rc = 0;
  if( z==0 || z[0]==0 ){
    emailerError(p, "missing \"%s\" setting", zName);
    rc = 1;
  }else{
    *pzVal = z;
  }
  return rc;
}

/*
** Create a new EmailSender object.
**




** The method used for sending email is determined by various email-*
** settings, and especially email-send-method.  The repository
** email-send-method can be overridden by the zAltDest argument to
** cause a different sending mechanism to be used.  Pass "stdout" to
** zAltDest to cause all emails to be printed to the console for
** debugging purposes.
**
** The EmailSender object returned must be freed using email_sender_free().
*/
EmailSender *email_sender_new(const char *zAltDest, int bImmediateFail){
  EmailSender *p;
  char *zMissing = 0;

  p = fossil_malloc(sizeof(*p));
  memset(p, 0, sizeof(*p));
  p->bImmediateFail = bImmediateFail;
  if( zAltDest ){
    p->zDest = zAltDest;
  }else{
    p->zDest = db_get("email-send-method","off");
  }
  if( fossil_strcmp(p->zDest,"off")==0 ) return p;
  if( emailerGetSetting(p, &p->zFrom, "email-self") ) return p;
  if( fossil_strcmp(p->zDest,"db")==0 ){
    char *zErr;
    int rc;
    if( emailerGetSetting(p, &p->zDb, "email-send-db") ) return p;
    rc = sqlite3_open(p->zDb, &p->db);
    if( rc ){
      emailerError(p, "unable to open output database file \"%s\": %s",
                   p->zDb, sqlite3_errmsg(p->db));
      return p;
    }
    rc = sqlite3_exec(p->db, "CREATE TABLE IF NOT EXISTS email(\n"
                          "  emailid INTEGER PRIMARY KEY,\n"
                          "  msg TEXT\n);", 0, 0, &zErr);
    if( zErr ){
      emailerError(p, "CREATE TABLE failed with \"%s\"", zErr);
      sqlite3_free(zErr);
      return p;
    }
    rc = sqlite3_prepare_v2(p->db, "INSERT INTO email(msg) VALUES(?1)", -1,
                            &p->pStmt, 0);
    if( rc ){
      emailerError(p, "cannot prepare INSERT statement: %s",
                 sqlite3_errmsg(p->db));
      return p;
    }
  }else if( fossil_strcmp(p->zDest, "pipe")==0 ){
    emailerGetSetting(p, &p->zCmd, "email-send-command");
  }else if( fossil_strcmp(p->zDest, "dir")==0 ){
    emailerGetSetting(p, &p->zDir, "email-send-dir");
  }
  return p;
}

/*
** Send a single email message.
**
** The recepient(s) must be specified using  "To:" or "Cc:" or "Bcc:" fields
** in the header.  Likewise, the header must contains a "Subject:" line.
** The header might also include fields like "Message-Id:" or
** "In-Reply-To:".
**
** This routine will add fields to the header as follows:
**
**     From:
**     Content-Type:
**     Content-Transfer-Encoding:
**     


** The caller maintains ownership of the input Blobs.  This routine will
** read the Blobs and send them onward to the email system, but it will
** not free them.



*/



void email_send(EmailSender *p, Blob *pHdr, Blob *pBody){
  Blob all;





  if( fossil_strcmp(p->zDest, "off")==0 ){
    return;
  }
  blob_init(&all, 0, 0);
  blob_append(&all, blob_buffer(pHdr), blob_size(pHdr));
  blob_appendf(&all, "From: %s\r\n", p->zFrom);







  blob_add_final_newline(pBody);



  blob_appendf(&all,"Content-Type: text/plain\r\n");
  blob_appendf(&all, "Content-Transfer-Encoding: base64\r\n\r\n");
  append_base64(&all, pBody);

  if( p->pStmt ){
















    int i, rc;









    sqlite3_bind_text(p->pStmt, 1, blob_str(&all), -1, SQLITE_TRANSIENT);
    for(i=0; i<100 && sqlite3_step(p->pStmt)==SQLITE_BUSY; i++){
      sqlite3_sleep(10);

    }

    rc = sqlite3_reset(p->pStmt);
    if( rc!=SQLITE_OK ){
      emailerError(p, "Failed to insert email message into output queue.\n"
                      "%s", sqlite3_errmsg(p->db));
    }


  }else if( p->zCmd ){
    FILE *out = popen(p->zCmd, "w");
    if( out ){
      fwrite(blob_buffer(&all), 1, blob_size(&all), out);
      fclose(out);
    }else{
      emailerError(p, "Could not open output pipe \"%s\"", p->zCmd);
    }



  }else if( p->zDir ){
    char *zFile = emailTempFilename(p->zDir);
    blob_write_to_file(&all, zFile);
    fossil_free(zFile);
  }else if( strcmp(p->zDest, "stdout")==0 ){
    fossil_print("%s\n", blob_str(&all));
  }
  blob_zero(&all);
}

/*
** Analyze and act on a received email.
................................................................................
**
**    send TO [OPTIONS]       Send a single email message using whatever
**                            email sending mechanism is currently configured.
**                            Use this for testing the email configuration.
**                            Options:
**
**                              --body FILENAME

**                              --stdout
**                              --subject|-S SUBJECT
**
**    settings [NAME VALUE]   With no arguments, list all email settings.
**                            Or change the value of a single email setting.
**
**    subscribers [PATTERN]   List all subscribers matching PATTERN.
................................................................................
        "DROP TABLE IF EXISTS subscription;\n"
      );
      email_schema();
    }
  }else
  if( strncmp(zCmd, "send", nCmd)==0 ){
    Blob prompt, body, hdr;


    const char *zDest = find_option("stdout",0,0)!=0 ? "stdout" : 0;
    int i;
    const char *zSubject = find_option("subject", "S", 1);
    const char *zSource = find_option("body", 0, 1);
    EmailSender *pSender;
    verify_all_options();
    blob_init(&prompt, 0, 0);
    blob_init(&body, 0, 0);
    blob_init(&hdr, 0, 0);
    for(i=3; i<g.argc; i++){
      blob_appendf(&hdr, "To: %s\n", g.argv[i]);
    }
................................................................................
    }
    if( zSource ){
      blob_read_from_file(&body, zSource, ExtFILE);
    }else{
      prompt_for_user_comment(&body, &prompt);
    }
    blob_add_final_newline(&body);
    pSender = email_sender_new(zDest, 1);
    email_send(pSender, &hdr, &body);
    email_sender_free(pSender);








    blob_zero(&hdr);
    blob_zero(&body);
    blob_zero(&prompt);
  }else
  if( strncmp(zCmd, "settings", nCmd)==0 ){
    int isGlobal = find_option("global",0,0)!=0;
    int nSetting;
................................................................................
      ** No verification is required.  Jump immediately to /alerts page.
      */
      cgi_redirectf("%R/alerts/%s", zCode);
      return;
    }else{
      /* We need to send a verification email */
      Blob hdr, body;
      EmailSender *pSender = email_sender_new(0,0);
      blob_init(&hdr,0,0);
      blob_init(&body,0,0);
      blob_appendf(&hdr, "To: %s\n", zEAddr);
      blob_appendf(&hdr, "Subject: Subscription verification\n");
      blob_appendf(&body, zConfirmMsg/*works-like:"%s%s%s"*/,
                   g.zBaseURL, g.zBaseURL, zCode);
      email_send(pSender, &hdr, &body);
      style_header("Email Alert Verification");
      if( pSender->zErr ){
        @ <h1>Internal Error</h1>
        @ <p>The following internal error was encountered while trying
        @ to send the confirmation email:
        @ <blockquote><pre>
        @ %h(pSender->zErr)
        @ </pre></blockquote>
      }else{
        @ <p>An email has been sent to "%h(zEAddr)". That email contains a
        @ hyperlink that you must click on in order to activate your
        @ subscription.</p>
      }
      email_sender_free(pSender);
      style_footer();
    }
    return;
  }
  style_header("Signup For Email Alerts");
  @ <p>To receive email notifications for changes to this
  @ repository, fill out the form below and press "Submit" button.</p>
................................................................................
      bSubmit = 0;
    }
  }
  if( bSubmit ){
    /* If we get this far, it means that a valid unsubscribe request has
    ** been submitted.  Send the appropriate email. */
    Blob hdr, body;
    EmailSender *pSender = email_sender_new(0,0);
    blob_init(&hdr,0,0);
    blob_init(&body,0,0);
    blob_appendf(&hdr, "To: %s\n", zEAddr);
    blob_appendf(&hdr, "Subject: Unsubscribe Instructions\n");
    blob_appendf(&body, zUnsubMsg/*works-like:"%s%s%s%s%s%s"*/,
                  g.zBaseURL, g.zBaseURL, zCode, g.zBaseURL, g.zBaseURL, zCode);
    email_send(pSender, &hdr, &body);
    style_header("Unsubscribe Instructions Sent");
    if( pSender->zErr ){
      @ <h1>Internal Error</h1>
      @ <p>The following error was encountered while trying to send an
      @ email to %h(zEAddr):
      @ <blockquote><pre>
      @ %h(pSender->zErr)
      @ </pre></blockquote>
    }else{
      @ <p>An email has been sent to "%h(zEAddr)" that explains how to
      @ unsubscribe and/or modify your subscription settings</p>
    }
    email_sender_free(pSender);
    style_footer();
    return;
  }  

  /* Non-logged-in users have to enter an email address to which is
  ** sent a message containing the unsubscribe link.
  */
................................................................................
  Stmt q;
  const char *zDigest = "false";
  Blob hdr, body;
  const char *zUrl;
  const char *zRepoName;
  const char *zFrom;
  const char *zDest = (flags & SENDALERT_STDOUT) ? "stdout" : 0;
  EmailSender *pSender = 0;

  db_begin_transaction();
  if( !email_enabled() ) goto send_alerts_done;
  zUrl = db_get("email-url",0);
  if( zUrl==0 ) goto send_alerts_done;
  zRepoName = db_get("email-subname",0);
  if( zRepoName==0 ) goto send_alerts_done;
  zFrom = db_get("email-self",0);
  if( zFrom==0 ) goto send_alerts_done;
  pSender = email_sender_new(zDest, 0);
  db_multi_exec(
    "DROP TABLE IF EXISTS temp.wantalert;"
    "CREATE TEMP TABLE wantalert(eventId TEXT);"
  );
  if( flags & SENDALERT_DIGEST ){
    db_multi_exec(
      "INSERT INTO wantalert SELECT eventid FROM pending_alert"
................................................................................
      nHit++;
      blob_append(&body, "\n", 1);
      blob_append(&body, blob_buffer(&p->txt), blob_size(&p->txt));
    }
    if( nHit==0 ) continue;
    blob_appendf(&body,"\n%.72c\nSubscription info: %s/alerts/%s\n",
         '-', zUrl, zCode);
    email_send(pSender,&hdr,&body);
    blob_truncate(&hdr);
    blob_truncate(&body);
  }
  blob_zero(&hdr);
  blob_zero(&body);
  db_finalize(&q);
  email_free_eventlist(pEvents);
................................................................................
      db_multi_exec("UPDATE pending_alert SET sentDigest=true");
    }else{
      db_multi_exec("UPDATE pending_alert SET sentSep=true");
    }
    db_multi_exec("DELETE FROM pending_alert WHERE sentDigest AND sentSep");
  }
send_alerts_done:
  email_sender_free(pSender);
  db_end_transaction(0);
}