Many hyperlinks are disabled.
Use anonymous login
to enable hyperlinks.
Overview
Comment: | Add "SMTP relay" as a new method for sending alert emails. |
---|---|
Downloads: | Tarball | ZIP archive |
Timelines: | family | ancestors | descendants | both | smtp |
Files: | files | file ages | folders |
SHA3-256: |
b96415f047707df245270cbe40bd72f4 |
User & Date: | drh 2018-06-30 17:27:27.170 |
Context
2018-06-30
| ||
18:29 | Fixes to the "SMTP relay" alert send method. Add the --smtp-trace option to the "fossil email send" command. Expose and document the "email-send-relayhost" setting. ... (check-in: 006cc814 user: drh tags: smtp) | |
17:27 | Add "SMTP relay" as a new method for sending alert emails. ... (check-in: b96415f0 user: drh tags: smtp) | |
16:06 | Merge fixes and improvements from trunk. ... (check-in: 4f30802a user: drh tags: smtp) | |
Changes
Changes to src/email.c.
︙ | ︙ | |||
186 187 188 189 190 191 192 | ** WEBPAGE: setup_email ** ** Administrative page for configuring and controlling email notification. ** Normally accessible via the /Admin/Email menu. */ void setup_email(void){ static const char *const azSendMethods[] = { | | | | | > | 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 | ** WEBPAGE: setup_email ** ** Administrative page for configuring and controlling email notification. ** Normally accessible via the /Admin/Email menu. */ void setup_email(void){ static const char *const azSendMethods[] = { "off", "Disabled", "pipe", "Pipe to a command", "db", "Store in a database", "dir", "Store in a directory", "relay", "SMTP relay" }; login_check_credentials(); if( !g.perm.Setup ){ login_needed(0); return; } db_begin_transaction(); |
︙ | ︙ | |||
242 243 244 245 246 247 248 | @ need to have a cron job running to invoke "fossil email exec" @ periodically. @ (Property: "email-autoexec")</p> @ <hr> multiple_choice_attribute("Email Send Method", "email-send-method", "esm", "off", count(azSendMethods)/2, azSendMethods); | | < | < < | > > > > > > > > > | 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 | @ need to have a cron job running to invoke "fossil email exec" @ periodically. @ (Property: "email-autoexec")</p> @ <hr> multiple_choice_attribute("Email Send Method", "email-send-method", "esm", "off", count(azSendMethods)/2, azSendMethods); @ <p>How to send email. Requires auxiliary information from the fields @ that follow. (Property: "email-send-method")</p> email_schema(1); entry_attribute("Command To Pipe Email To", 60, "email-send-command", "ecmd", "sendmail -t", 0); @ <p>When the send method is "pipe to a command", this is the command @ that is run. Email messages are piped into the standard input of this @ command. The command is expected to extract the sender address, @ recepient addresses, and subject from the header of the piped email @ text. (Property: "email-send-command")</p> entry_attribute("Database In Which To Store Email", 60, "email-send-db", "esdb", "", 0); @ <p>When the send method is "store in a databaes", each email message is @ stored in an SQLite database file with the name given here. @ (Property: "email-send-db")</p> entry_attribute("Directory In Which To Store Email", 60, "email-send-dir", "esdir", "", 0); @ <p>When the send method is "store in a directory", each email message is @ stored as a separate file in the directory shown here. @ (Property: "email-send-dir")</p> entry_attribute("SMTP relay host", 60, "email-send-relayhost", "esrh", "", 0); @ <p>When the send method is "SMTP relay", each email message is @ transmitted via the SMTP protocol (rfc5321) to a "Mail Submission @ Agent" or "MSA" (rfc4409) at the hostname shown here. Optionally @ append a colon and TCP port number (ex: smtp.example.com:587). @ The default TCP port number is 25. @ (Property: "email-send-relayhost")</p> @ <hr> entry_attribute("Administrator email address", 40, "email-admin", "eadmin", "", 0); @ <p>This is the email for the human administrator for the system. @ Abuse and trouble reports are send here. @ (Property: "email-admin")</p> |
︙ | ︙ | |||
374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 | 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 */ Blob out; /* For zDest=="blob" */ 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_close(p->db); p->db = 0; p->zDb = 0; p->zDir = 0; p->zCmd = 0; | > > > | > | 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 | 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 */ SmtpSession *pSmtp; /* SMTP relay connection */ Blob out; /* For zDest=="blob" */ 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_close(p->db); p->db = 0; p->zDb = 0; p->zDir = 0; p->zCmd = 0; if( p->pSmtp ){ smtp_session_free(p->pSmtp); p->pSmtp = 0; } blob_reset(&p->out); } /* ** Put the EmailSender into an error state. */ static void emailerError(EmailSender *p, const char *zFormat, ...){ |
︙ | ︙ | |||
458 459 460 461 462 463 464 465 466 467 468 469 470 471 | ** The EmailSender object returned must be freed using email_sender_free(). */ EmailSender *email_sender_new(const char *zAltDest, int bImmediateFail){ EmailSender *p; 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; | > | 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 | ** The EmailSender object returned must be freed using email_sender_free(). */ EmailSender *email_sender_new(const char *zAltDest, int bImmediateFail){ EmailSender *p; p = fossil_malloc(sizeof(*p)); memset(p, 0, sizeof(*p)); blob_init(&p->out, 0, 0); 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; |
︙ | ︙ | |||
497 498 499 500 501 502 503 | } }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"); }else if( fossil_strcmp(p->zDest, "blob")==0 ){ blob_init(&p->out, 0, 0); | > > > > > | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 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 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 | } }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"); }else if( fossil_strcmp(p->zDest, "blob")==0 ){ blob_init(&p->out, 0, 0); }else if( fossil_strcmp(p->zDest, "relay")==0 ){ const char *zRelay = 0; emailerGetSetting(p, &zRelay, "email-send-relayhost"); if( zRelay ){ p->pSmtp = smtp_session_new(p->zFrom, zRelay, SMTP_DIRECT); } } return p; } /* ** Scan the header of the email message in pMsg looking for the ** (first) occurrance of zField. Fill pValue with the content of ** that field. ** ** This routine initializes pValue. Any prior content of pValue is ** discarded (leaked). ** ** Return non-zero on success. Return 0 if no instance of the header ** is found. */ int email_header_value(Blob *pMsg, const char *zField, Blob *pValue){ int nField = (int)strlen(zField); Blob line; blob_rewind(pMsg); blob_init(pValue,0,0); while( blob_line(pMsg, &line) ){ int n, i; char *z; blob_trim(&line); n = blob_size(&line); if( n==0 ) return 0; if( n<nField+1 ) continue; z = blob_buffer(&line); if( sqlite3_strnicmp(z, zField, nField)==0 && z[nField]==':' ){ for(i=nField+1; i<n && fossil_isspace(z[i]); i++){} blob_init(pValue, z+i, n-i); while( blob_line(pMsg, &line) ){ blob_trim(&line); n = blob_size(&line); if( n==0 ) break; z = blob_buffer(&line); if( !fossil_isspace(z[0]) ) break; for(i=1; i<n && fossil_isspace(z[i]); i++){} blob_append(pValue, " ", 1); blob_append(pValue, z+i, n-i); } return 1; } } return 0; } /* ** Make a copy of the input string up to but not including the ** first ">" character. ** ** Verify that the string really that is to be copied really is a ** valid email address. If it is not, then return NULL. ** ** This routine is more restrictive than necessary. It does not ** allow comments, IP address, quoted strings, or certain uncommon ** characters. The only non-alphanumerics allowed in the local ** part are "_", "+", "-" and "+". */ char *email_copy_addr(const char *z){ int i; int nAt = 0; int nDot = 0; char c; if( z[0]=='.' ) return 0; /* Local part cannot begin with "." */ for(i=0; (c = z[i])!=0 && c!='>'; i++){ if( fossil_isalnum(c) ){ /* Alphanumerics are always ok */ }else if( c=='@' ){ if( nAt ) return 0; /* Only a single "@" allowed */ if( i>64 ) return 0; /* Local part too big */ nAt = 1; nDot = 0; if( i==0 ) return 0; /* Disallow empty local part */ if( z[i-1]=='.' ) return 0; /* Last char of local cannot be "." */ if( z[i+1]=='.' || z[i+1]=='-' ){ return 0; /* Domain cannot begin with "." or "-" */ } }else if( c=='-' ){ if( z[i+1]=='>' ) return 0; /* Last character cannot be "-" */ }else if( c=='.' ){ if( z[i+1]=='.' ) return 0; /* Do not allow ".." */ if( z[i+1]=='>' ) return 0; /* Domain may not end with . */ nDot++; }else if( (c=='_' || c=='+') && nAt==0 ){ /* _ and + are ok in the local part */ }else{ return 0; /* Anything else is an error */ } } if( c!='>' ) return 0; /* Missing final ">" */ if( nAt==0 ) return 0; /* No "@" found anywhere */ if( nDot==0 ) return 0; /* No "." in the domain */ /* If we reach this point, the email address is valid */ return mprintf("%.*s", i, z); } /* ** Extract all To: header values from the email header supplied. ** Store them in the array list. */ void email_header_to(Blob *pMsg, int *pnTo, char ***pazTo){ int nTo = 0; char **azTo = 0; Blob v; char *z, *zAddr; int i; email_header_value(pMsg, "to", &v); z = blob_str(&v); for(i=0; z[i]; i++){ if( z[i]=='<' && (zAddr = email_copy_addr(&z[i+1]))!=0 ){ azTo = fossil_realloc(azTo, sizeof(azTo[0])*(nTo+1) ); azTo[nTo++] = zAddr; } } *pnTo = nTo; *pazTo = azTo; } /* ** Free a list of To addresses obtained from a prior call to ** email_header_to() */ void email_header_to_free(int nTo, char **azTo){ int i; for(i=0; i<nTo; i++) fossil_free(azTo[i]); fossil_free(azTo); } /* ** 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 |
︙ | ︙ | |||
577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 | }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 ){ blob_add_final_newline(&all); fossil_print("%s", blob_str(&all)); } blob_reset(&all); } /* | > > > > > > > > > > > > > > > > | 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 | }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( p->pSmtp ){ char **azTo = 0; int nTo = 0; email_header_to(pHdr, &nTo, &azTo); if( nTo>0 ){ smtp_send_msg(p->pSmtp, p->zFrom, nTo, azTo, blob_str(&all)); email_header_to_free(nTo, azTo); } }else if( strcmp(p->zDest, "stdout")==0 ){ char **azTo = 0; int nTo = 0; int i; email_header_to(pHdr, &nTo, &azTo); for(i=0; i<nTo; i++){ fossil_print("X-To-Test-%d: [%s]\r\n", i, azTo[i]); } email_header_to_free(nTo, azTo); blob_add_final_newline(&all); fossil_print("%s", blob_str(&all)); } blob_reset(&all); } /* |
︙ | ︙ |
Changes to src/smtp.c.
︙ | ︙ | |||
180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 | p->logFile = va_arg(ap, FILE*); } if( smtpFlags & SMTP_TRACE_BLOB ){ p->pTranscript = va_arg(ap, Blob*); } va_end(ap); if( (smtpFlags & SMTP_DIRECT)!=0 ){ p->zHostname = fossil_strdup(zDest); }else{ p->zHostname = smtp_mx_host(zDest); } if( p->zHostname==0 ){ p->atEof = 1; p->zErr = mprintf("cannot locate SMTP server for \"%s\"", zDest); return p; | > > > > > > | 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 | p->logFile = va_arg(ap, FILE*); } if( smtpFlags & SMTP_TRACE_BLOB ){ p->pTranscript = va_arg(ap, Blob*); } va_end(ap); if( (smtpFlags & SMTP_DIRECT)!=0 ){ int i; p->zHostname = fossil_strdup(zDest); for(i=0; p->zHostname[i] && p->zHostname[i]!=':'; i++){} if( p->zHostname[i]==':' ){ p->zHostname[i] = 0; url.port = atoi(&p->zHostname[i+1]); } }else{ p->zHostname = smtp_mx_host(zDest); } if( p->zHostname==0 ){ p->atEof = 1; p->zErr = mprintf("cannot locate SMTP server for \"%s\"", zDest); return p; |
︙ | ︙ | |||
469 470 471 472 473 474 475 | blob_reset(&f); } /* ** Send a single email message to the SMTP server. ** ** All email addresses (zFrom and azTo) must be plain "local@domain" | | | 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 | blob_reset(&f); } /* ** Send a single email message to the SMTP server. ** ** All email addresses (zFrom and azTo) must be plain "local@domain" ** format without the surrounding "<..>". This routine will add the ** necessary "<..>". ** ** The body of the email should be well-structured. This routine will ** convert any \n line endings into \r\n and will escape lines containing ** just ".", but will not make any other alterations or corrections to ** the message content. ** |
︙ | ︙ | |||
593 594 595 596 597 598 599 | blob_reset(&body); } /***************************************************************************** ** Server implementation *****************************************************************************/ | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | 599 600 601 602 603 604 605 606 607 608 609 610 611 612 | blob_reset(&body); } /***************************************************************************** ** Server implementation *****************************************************************************/ /* ** Schema used by the email processing system. */ static const char zEmailSchema[] = @ -- bulk storage is in a separate table. This table can store either @ -- the body of email messages or transcripts of smtp sessions. @ CREATE TABLE IF NOT EXISTS repository.emailblob( |
︙ | ︙ | |||
967 968 969 970 971 972 973 | /* Finish the transaction after all changes are implemented */ db_end_transaction(0); } smtp_server_clear(p, SMTPSRV_CLEAR_MSG); } | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | 930 931 932 933 934 935 936 937 938 939 940 941 942 943 | /* Finish the transaction after all changes are implemented */ db_end_transaction(0); } smtp_server_clear(p, SMTPSRV_CLEAR_MSG); } /* ** COMMAND: smtpd ** ** Usage: %fossil smtpd [OPTIONS] REPOSITORY ** ** Begin a SMTP conversation with a client using stdin/stdout. The ** received email is stored in REPOSITORY. |
︙ | ︙ | |||
1062 1063 1064 1065 1066 1067 1068 | }else if( strncmp(z, "HELO", 4)==0 && fossil_isspace(z[4]) ){ smtp_server_send(&x, "250 ok\r\n"); }else if( strncmp(z, "MAIL FROM:<", 11)==0 ){ smtp_server_route_incoming(&x, 0); smtp_server_clear(&x, SMTPSRV_CLEAR_MSG); | | | | 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 | }else if( strncmp(z, "HELO", 4)==0 && fossil_isspace(z[4]) ){ smtp_server_send(&x, "250 ok\r\n"); }else if( strncmp(z, "MAIL FROM:<", 11)==0 ){ smtp_server_route_incoming(&x, 0); smtp_server_clear(&x, SMTPSRV_CLEAR_MSG); x.zFrom = email_copy_addr(z+11); if( x.zFrom==0 ){ smtp_server_send(&x, "500 unacceptable email address\r\n"); }else{ smtp_server_send(&x, "250 ok\r\n"); } }else if( strncmp(z, "RCPT TO:<", 9)==0 ){ char *zAddr; if( x.zFrom==0 ){ smtp_server_send(&x, "500 missing MAIL FROM\r\n"); continue; } zAddr = email_copy_addr(z+9); if( zAddr==0 ){ smtp_server_send(&x, "505 no such user\r\n"); continue; } smtp_append_to(&x, zAddr, 0); if( x.nTo>=100 ){ smtp_server_send(&x, "452 too many recipients\r\n"); |
︙ | ︙ |