Fossil SCM
Improvements to email notification. Rename the "email" command to "alerts". Try to avoid sending alerts about unapproved events, except it is ok to send such alerts to moderators. This is a work in progress.
Commit
efbd6caa7c9d2c1487d9631fbaeb88bfbcf0b9b62e4f12a5dae642045c2bc2c9
Parent
ce3d5718370a7e5…
1 file changed
+128
-71
+128
-71
| --- src/email.c | ||
| +++ src/email.c | ||
| @@ -77,12 +77,13 @@ | ||
| 77 | 77 | @ -- Remaining characters determine the specific event. For example, |
| 78 | 78 | @ -- 'c4413' means check-in with rid=4413. |
| 79 | 79 | @ -- |
| 80 | 80 | @ CREATE TABLE repository.pending_alert( |
| 81 | 81 | @ eventid TEXT PRIMARY KEY, -- Object that changed |
| 82 | -@ sentSep BOOLEAN DEFAULT false, -- individual emails sent | |
| 83 | -@ sentDigest BOOLEAN DEFAULT false -- digest emails sent | |
| 82 | +@ sentSep BOOLEAN DEFAULT false, -- individual alert sent | |
| 83 | +@ sentDigest BOOLEAN DEFAULT false, -- digest alert sent | |
| 84 | +@ sentMod BOOLEAN DEFAULT false -- pending moderation alert sent | |
| 84 | 85 | @ ) WITHOUT ROWID; |
| 85 | 86 | @ |
| 86 | 87 | @ DROP TABLE IF EXISTS repository.email_bounce; |
| 87 | 88 | @ -- Record bounced emails. If too many bounces are received within |
| 88 | 89 | @ -- some defined time range, then cancel the subscription. Older |
| @@ -115,10 +116,15 @@ | ||
| 115 | 116 | ){ |
| 116 | 117 | return; /* Don't create table for disabled email */ |
| 117 | 118 | } |
| 118 | 119 | db_multi_exec(zEmailInit/*works-like:""*/); |
| 119 | 120 | email_triggers_enable(); |
| 121 | + }else if( !db_table_has_column("repository","pending_alert","sentMod") ){ | |
| 122 | + db_multi_exec( | |
| 123 | + "ALTER TABLE repository.pending_alert" | |
| 124 | + " ADD COLUMN sentMod BOOLEAN DEFAULT false;" | |
| 125 | + ); | |
| 120 | 126 | } |
| 121 | 127 | } |
| 122 | 128 | |
| 123 | 129 | /* |
| 124 | 130 | ** Enable triggers that automatically populate the pending_alert |
| @@ -294,18 +300,10 @@ | ||
| 294 | 300 | @ <p>This is the email for the human administrator for the system. |
| 295 | 301 | @ Abuse and trouble reports are send here. |
| 296 | 302 | @ (Property: "email-admin")</p> |
| 297 | 303 | @ <hr> |
| 298 | 304 | |
| 299 | - entry_attribute("Inbound email directory", 40, "email-receive-dir", | |
| 300 | - "erdir", "", 0); | |
| 301 | - @ <p>Inbound emails can be stored in a directory for analysis as | |
| 302 | - @ a debugging aid. Put the name of that directory in this entry box. | |
| 303 | - @ Disable saving of inbound email by making this an empty string. | |
| 304 | - @ Abuse and trouble reports are send here. | |
| 305 | - @ (Property: "email-receive-dir")</p> | |
| 306 | - @ <hr> | |
| 307 | 305 | @ <p><input type="submit" name="submit" value="Apply Changes" /></p> |
| 308 | 306 | @ </div></form> |
| 309 | 307 | db_end_transaction(0); |
| 310 | 308 | style_footer(); |
| 311 | 309 | } |
| @@ -757,24 +755,10 @@ | ||
| 757 | 755 | fossil_print("%s", blob_str(&all)); |
| 758 | 756 | } |
| 759 | 757 | blob_reset(&all); |
| 760 | 758 | } |
| 761 | 759 | |
| 762 | -/* | |
| 763 | -** Analyze and act on a received email. | |
| 764 | -** | |
| 765 | -** This routine takes ownership of the Blob parameter and is responsible | |
| 766 | -** for freeing that blob when it is done with it. | |
| 767 | -** | |
| 768 | -** This routine acts on all email messages received from the | |
| 769 | -** "fossil email inbound" command. | |
| 770 | -*/ | |
| 771 | -void email_receive(Blob *pMsg){ | |
| 772 | - /* To Do: Look for bounce messages and possibly disable subscriptions */ | |
| 773 | - blob_reset(pMsg); | |
| 774 | -} | |
| 775 | - | |
| 776 | 760 | /* |
| 777 | 761 | ** SETTING: email-send-method width=5 default=off |
| 778 | 762 | ** Determine the method used to send email. Allowed values are |
| 779 | 763 | ** "off", "relay", "pipe", "dir", "db", and "stdout". The "off" value |
| 780 | 764 | ** means no email is ever sent. The "relay" value means emails are sent |
| @@ -806,16 +790,10 @@ | ||
| 806 | 790 | /* |
| 807 | 791 | ** SETTING: email-self width=40 |
| 808 | 792 | ** This is the email address for the repository. Outbound emails add |
| 809 | 793 | ** this email address as the "From:" field. |
| 810 | 794 | */ |
| 811 | -/* | |
| 812 | -** SETTING: email-receive-dir width=40 | |
| 813 | -** Inbound email messages are saved as separate files in this directory, | |
| 814 | -** for debugging analysis. Disable saving of inbound emails omitting | |
| 815 | -** this setting, or making it an empty string. | |
| 816 | -*/ | |
| 817 | 795 | /* |
| 818 | 796 | ** SETTING: email-send-relayhost width=40 |
| 819 | 797 | ** This is the hostname and TCP port to which output email messages |
| 820 | 798 | ** are sent when email-send-method is "relay". There should be an |
| 821 | 799 | ** SMTP server configured as a Mail Submission Agent listening on the |
| @@ -822,13 +800,13 @@ | ||
| 822 | 800 | ** designated host and port and all times. |
| 823 | 801 | */ |
| 824 | 802 | |
| 825 | 803 | |
| 826 | 804 | /* |
| 827 | -** COMMAND: email | |
| 805 | +** COMMAND: alerts | |
| 828 | 806 | ** |
| 829 | -** Usage: %fossil email SUBCOMMAND ARGS... | |
| 807 | +** Usage: %fossil alerts SUBCOMMAND ARGS... | |
| 830 | 808 | ** |
| 831 | 809 | ** Subcommands: |
| 832 | 810 | ** |
| 833 | 811 | ** exec Compose and send pending email alerts. |
| 834 | 812 | ** Some installations may want to do this via |
| @@ -837,13 +815,11 @@ | ||
| 837 | 815 | ** Options: |
| 838 | 816 | ** |
| 839 | 817 | ** --digest Send digests |
| 840 | 818 | ** --test Resets to standard output |
| 841 | 819 | ** |
| 842 | -** inbound [FILE] Receive an inbound email message. This message | |
| 843 | -** is analyzed to see if it is a bounce, and if | |
| 844 | -** necessary, subscribers may be disabled. | |
| 820 | +** pending Show all pending alerts. Useful for debugging. | |
| 845 | 821 | ** |
| 846 | 822 | ** reset Hard reset of all email notification tables |
| 847 | 823 | ** in the repository. This erases all subscription |
| 848 | 824 | ** information. Use with extreme care. |
| 849 | 825 | ** |
| @@ -857,10 +833,13 @@ | ||
| 857 | 833 | ** --stdout |
| 858 | 834 | ** --subject|-S SUBJECT |
| 859 | 835 | ** |
| 860 | 836 | ** settings [NAME VALUE] With no arguments, list all email settings. |
| 861 | 837 | ** Or change the value of a single email setting. |
| 838 | +** | |
| 839 | +** status Report on the status of the email alert | |
| 840 | +** subsystem | |
| 862 | 841 | ** |
| 863 | 842 | ** subscribers [PATTERN] List all subscribers matching PATTERN. |
| 864 | 843 | ** |
| 865 | 844 | ** unsubscribe EMAIL Remove a single subscriber with the given EMAIL. |
| 866 | 845 | */ |
| @@ -878,24 +857,24 @@ | ||
| 878 | 857 | eFlags |= SENDALERT_PRESERVE|SENDALERT_STDOUT; |
| 879 | 858 | } |
| 880 | 859 | verify_all_options(); |
| 881 | 860 | email_send_alerts(eFlags); |
| 882 | 861 | }else |
| 883 | - if( strncmp(zCmd, "inbound", nCmd)==0 ){ | |
| 884 | - Blob email; | |
| 885 | - const char *zInboundDir = db_get("email-receive-dir",""); | |
| 862 | + if( strncmp(zCmd, "pending", nCmd)==0 ){ | |
| 863 | + Stmt q; | |
| 886 | 864 | verify_all_options(); |
| 887 | - if( g.argc!=3 && g.argc!=4 ){ | |
| 888 | - usage("inbound [FILE]"); | |
| 889 | - } | |
| 890 | - blob_read_from_file(&email, g.argc==3 ? "-" : g.argv[3], ExtFILE); | |
| 891 | - if( zInboundDir[0] ){ | |
| 892 | - char *zFN = file_time_tempname(zInboundDir,".email"); | |
| 893 | - blob_write_to_file(&email, zFN); | |
| 894 | - fossil_free(zFN); | |
| 895 | - } | |
| 896 | - email_receive(&email); | |
| 865 | + if( g.argc!=3 ) usage("pending"); | |
| 866 | + db_prepare(&q,"SELECT eventid, sentSep, sentDigest, sentMod" | |
| 867 | + " FROM pending_alert"); | |
| 868 | + while( db_step(&q)==SQLITE_ROW ){ | |
| 869 | + fossil_print("%10s %7s %10s %7s\n", | |
| 870 | + db_column_text(&q,0), | |
| 871 | + db_column_int(&q,1) ? "sentSep" : "", | |
| 872 | + db_column_int(&q,2) ? "sentDigest" : "", | |
| 873 | + db_column_int(&q,3) ? "sentMod" : ""); | |
| 874 | + } | |
| 875 | + db_finalize(&q); | |
| 897 | 876 | }else |
| 898 | 877 | if( strncmp(zCmd, "reset", nCmd)==0 ){ |
| 899 | 878 | int c; |
| 900 | 879 | int bForce = find_option("force","f",0)!=0; |
| 901 | 880 | verify_all_options(); |
| @@ -979,10 +958,33 @@ | ||
| 979 | 958 | for(; nSetting>0; nSetting--, pSetting++ ){ |
| 980 | 959 | if( strncmp(pSetting->name,"email-",6)!=0 ) continue; |
| 981 | 960 | print_setting(pSetting); |
| 982 | 961 | } |
| 983 | 962 | }else |
| 963 | + if( strncmp(zCmd, "status", nCmd)==0 ){ | |
| 964 | + int isGlobal = find_option("global",0,0)!=0; | |
| 965 | + int nSetting, n; | |
| 966 | + static const char *zFmt = "%-29s %d\n"; | |
| 967 | + const Setting *pSetting = setting_info(&nSetting); | |
| 968 | + db_open_config(1, 0); | |
| 969 | + verify_all_options(); | |
| 970 | + if( g.argc!=3 ) usage("status"); | |
| 971 | + pSetting = setting_info(&nSetting); | |
| 972 | + for(; nSetting>0; nSetting--, pSetting++ ){ | |
| 973 | + if( strncmp(pSetting->name,"email-",6)!=0 ) continue; | |
| 974 | + print_setting(pSetting); | |
| 975 | + } | |
| 976 | + n = db_int(0,"SELECT count(*) FROM pending_alert WHERE NOT sentSep"); | |
| 977 | + fossil_print(zFmt/*works-like:"%s%d"*/, "pending-alerts", n); | |
| 978 | + n = db_int(0,"SELECT count(*) FROM pending_alert WHERE NOT sentDigest"); | |
| 979 | + fossil_print(zFmt/*works-like:"%s%d"*/, "pending-digest-alerts", n); | |
| 980 | + n = db_int(0,"SELECT count(*) FROM subscriber"); | |
| 981 | + fossil_print(zFmt/*works-like:"%s%d"*/, "total-subscribers", n); | |
| 982 | + n = db_int(0, "SELECT count(*) FROM subscriber WHERE sverified" | |
| 983 | + " AND NOT sdonotcall AND length(ssub)>1"); | |
| 984 | + fossil_print(zFmt/*works-like:"%s%d"*/, "active-subscribers", n); | |
| 985 | + }else | |
| 984 | 986 | if( strncmp(zCmd, "subscribers", nCmd)==0 ){ |
| 985 | 987 | Stmt q; |
| 986 | 988 | verify_all_options(); |
| 987 | 989 | if( g.argc!=3 && g.argc!=4 ) usage("subscribers [PATTERN]"); |
| 988 | 990 | if( g.argc==4 ){ |
| @@ -1008,11 +1010,11 @@ | ||
| 1008 | 1010 | if( g.argc!=4 ) usage("unsubscribe EMAIL"); |
| 1009 | 1011 | db_multi_exec( |
| 1010 | 1012 | "DELETE FROM subscriber WHERE semail=%Q", g.argv[3]); |
| 1011 | 1013 | }else |
| 1012 | 1014 | { |
| 1013 | - usage("exec|inbound|reset|send|setting|subscribers|unsubscribe"); | |
| 1015 | + usage("exec|pending|reset|send|setting|status|subscribers|unsubscribe"); | |
| 1014 | 1016 | } |
| 1015 | 1017 | } |
| 1016 | 1018 | |
| 1017 | 1019 | /* |
| 1018 | 1020 | ** Do error checking on a submitted subscription form. Return TRUE |
| @@ -1789,11 +1791,12 @@ | ||
| 1789 | 1791 | /* |
| 1790 | 1792 | ** A single event that might appear in an alert is recorded as an |
| 1791 | 1793 | ** instance of the following object. |
| 1792 | 1794 | */ |
| 1793 | 1795 | struct EmailEvent { |
| 1794 | - int type; /* 'c', 't', 'w', 'f' */ | |
| 1796 | + int type; /* 'c', 'f', 'm', 't', 'w' */ | |
| 1797 | + int needMod; /* Pending moderator approval */ | |
| 1795 | 1798 | Blob txt; /* Text description to appear in an alert */ |
| 1796 | 1799 | EmailEvent *pNext; /* Next in chronological order */ |
| 1797 | 1800 | }; |
| 1798 | 1801 | #endif |
| 1799 | 1802 | |
| @@ -1812,33 +1815,34 @@ | ||
| 1812 | 1815 | /* |
| 1813 | 1816 | ** Compute and return a linked list of EmailEvent objects |
| 1814 | 1817 | ** corresponding to the current content of the temp.wantalert |
| 1815 | 1818 | ** table which should be defined as follows: |
| 1816 | 1819 | ** |
| 1817 | -** CREATE TEMP TABLE wantalert(eventId TEXT); | |
| 1820 | +** CREATE TEMP TABLE wantalert(eventId TEXT, needMod BOOLEAN); | |
| 1818 | 1821 | */ |
| 1819 | -EmailEvent *email_compute_event_text(int *pnEvent){ | |
| 1822 | +EmailEvent *email_compute_event_text(int *pnEvent, int doDigest){ | |
| 1820 | 1823 | Stmt q; |
| 1821 | 1824 | EmailEvent *p; |
| 1822 | 1825 | EmailEvent anchor; |
| 1823 | 1826 | EmailEvent *pLast; |
| 1824 | 1827 | const char *zUrl = db_get("email-url","http://localhost:8080"); |
| 1825 | 1828 | |
| 1826 | 1829 | db_prepare(&q, |
| 1827 | 1830 | "SELECT" |
| 1828 | - " blob.uuid," /* 0 */ | |
| 1829 | - " datetime(event.mtime)," /* 1 */ | |
| 1831 | + " blob.uuid," /* 0 */ | |
| 1832 | + " datetime(event.mtime)," /* 1 */ | |
| 1830 | 1833 | " coalesce(ecomment,comment)" |
| 1831 | 1834 | " || ' (user: ' || coalesce(euser,user,'?')" |
| 1832 | 1835 | " || (SELECT case when length(x)>0 then ' tags: ' || x else '' end" |
| 1833 | 1836 | " FROM (SELECT group_concat(substr(tagname,5), ', ') AS x" |
| 1834 | 1837 | " FROM tag, tagxref" |
| 1835 | 1838 | " WHERE tagname GLOB 'sym-*' AND tag.tagid=tagxref.tagid" |
| 1836 | 1839 | " AND tagxref.rid=blob.rid AND tagxref.tagtype>0))" |
| 1837 | - " || ')' as comment," /* 2 */ | |
| 1840 | + " || ')' as comment," /* 2 */ | |
| 1838 | 1841 | " tagxref.value AS branch," /* 3 */ |
| 1839 | - " wantalert.eventId" /* 4 */ | |
| 1842 | + " wantalert.eventId," /* 4 */ | |
| 1843 | + " wantalert.needMod" /* 5 */ | |
| 1840 | 1844 | " FROM temp.wantalert JOIN tag CROSS JOIN event CROSS JOIN blob" |
| 1841 | 1845 | " LEFT JOIN tagxref ON tagxref.tagid=tag.tagid" |
| 1842 | 1846 | " AND tagxref.tagtype>0" |
| 1843 | 1847 | " AND tagxref.rid=blob.rid" |
| 1844 | 1848 | " WHERE blob.rid=event.objid" |
| @@ -1853,13 +1857,15 @@ | ||
| 1853 | 1857 | const char *zType = ""; |
| 1854 | 1858 | p = fossil_malloc( sizeof(EmailEvent) ); |
| 1855 | 1859 | pLast->pNext = p; |
| 1856 | 1860 | pLast = p; |
| 1857 | 1861 | p->type = db_column_text(&q, 4)[0]; |
| 1862 | + p->needMod = db_column_int(&q, 5); | |
| 1858 | 1863 | p->pNext = 0; |
| 1859 | 1864 | switch( p->type ){ |
| 1860 | 1865 | case 'c': zType = "Check-In"; break; |
| 1866 | + case 'f': zType = "Forum post"; break; | |
| 1861 | 1867 | case 't': zType = "Wiki Edit"; break; |
| 1862 | 1868 | case 'w': zType = "Ticket Change"; break; |
| 1863 | 1869 | } |
| 1864 | 1870 | blob_init(&p->txt, 0, 0); |
| 1865 | 1871 | blob_appendf(&p->txt,"== %s %s ==\n%s\n%s/info/%.20s\n", |
| @@ -1867,10 +1873,16 @@ | ||
| 1867 | 1873 | zType, |
| 1868 | 1874 | db_column_text(&q,2), |
| 1869 | 1875 | zUrl, |
| 1870 | 1876 | db_column_text(&q,0) |
| 1871 | 1877 | ); |
| 1878 | + if( p->needMod ){ | |
| 1879 | + blob_appendf(&p->txt, | |
| 1880 | + "** Pending moderator approval (%s/modreq) **\n", | |
| 1881 | + zUrl | |
| 1882 | + ); | |
| 1883 | + } | |
| 1872 | 1884 | (*pnEvent)++; |
| 1873 | 1885 | } |
| 1874 | 1886 | db_finalize(&q); |
| 1875 | 1887 | return anchor.pNext; |
| 1876 | 1888 | } |
| @@ -1905,32 +1917,44 @@ | ||
| 1905 | 1917 | ** command line, generate text for all events named in the |
| 1906 | 1918 | ** pending_alert table. |
| 1907 | 1919 | ** |
| 1908 | 1920 | ** This command is intended for testing and debugging the logic |
| 1909 | 1921 | ** that generates email alert text. |
| 1922 | +** | |
| 1923 | +** Options: | |
| 1924 | +** | |
| 1925 | +** --digest Generate digest alert text | |
| 1926 | +** --needmod Assume all events are pending moderator approval | |
| 1910 | 1927 | */ |
| 1911 | 1928 | void test_alert_cmd(void){ |
| 1912 | 1929 | Blob out; |
| 1913 | 1930 | int nEvent; |
| 1931 | + int needMod; | |
| 1932 | + int doDigest; | |
| 1914 | 1933 | EmailEvent *pEvent, *p; |
| 1915 | 1934 | |
| 1935 | + doDigest = find_option("digest",0,0)!=0; | |
| 1936 | + needMod = find_option("needmod",0,0)!=0; | |
| 1916 | 1937 | db_find_and_open_repository(0, 0); |
| 1917 | 1938 | verify_all_options(); |
| 1918 | 1939 | db_begin_transaction(); |
| 1919 | 1940 | email_schema(0); |
| 1920 | - db_multi_exec("CREATE TEMP TABLE wantalert(eventid TEXT)"); | |
| 1941 | + db_multi_exec("CREATE TEMP TABLE wantalert(eventid TEXT, needMod BOOLEAN)"); | |
| 1921 | 1942 | if( g.argc==2 ){ |
| 1922 | - db_multi_exec("INSERT INTO wantalert SELECT eventid FROM pending_alert"); | |
| 1943 | + db_multi_exec( | |
| 1944 | + "INSERT INTO wantalert(eventId,needMod)" | |
| 1945 | + " SELECT eventid, %d FROM pending_alert", needMod); | |
| 1923 | 1946 | }else{ |
| 1924 | 1947 | int i; |
| 1925 | 1948 | for(i=2; i<g.argc; i++){ |
| 1926 | - db_multi_exec("INSERT INTO wantalert VALUES(%Q)", g.argv[i]); | |
| 1949 | + db_multi_exec("INSERT INTO wantalert(eventId,needMod) VALUES(%Q,%d)", | |
| 1950 | + g.argv[i], needMod); | |
| 1927 | 1951 | } |
| 1928 | 1952 | } |
| 1929 | 1953 | blob_init(&out, 0, 0); |
| 1930 | 1954 | email_header(&out); |
| 1931 | - pEvent = email_compute_event_text(&nEvent); | |
| 1955 | + pEvent = email_compute_event_text(&nEvent, doDigest); | |
| 1932 | 1956 | for(p=pEvent; p; p=p->pNext){ |
| 1933 | 1957 | blob_append(&out, "\n", 1); |
| 1934 | 1958 | blob_append(&out, blob_buffer(&p->txt), blob_size(&p->txt)); |
| 1935 | 1959 | } |
| 1936 | 1960 | email_free_eventlist(pEvent); |
| @@ -2001,10 +2025,11 @@ | ||
| 2001 | 2025 | EmailSender *pSender = 0; |
| 2002 | 2026 | u32 senderFlags = 0; |
| 2003 | 2027 | |
| 2004 | 2028 | if( g.fSqlTrace ) fossil_trace("-- BEGIN email_send_alerts(%u)\n", flags); |
| 2005 | 2029 | db_begin_transaction(); |
| 2030 | + email_schema(0); | |
| 2006 | 2031 | if( !email_enabled() ) goto send_alerts_done; |
| 2007 | 2032 | zUrl = db_get("email-url",0); |
| 2008 | 2033 | if( zUrl==0 ) goto send_alerts_done; |
| 2009 | 2034 | zRepoName = db_get("email-subname",0); |
| 2010 | 2035 | if( zRepoName==0 ) goto send_alerts_done; |
| @@ -2014,25 +2039,35 @@ | ||
| 2014 | 2039 | senderFlags |= EMAIL_TRACE; |
| 2015 | 2040 | } |
| 2016 | 2041 | pSender = email_sender_new(zDest, senderFlags); |
| 2017 | 2042 | db_multi_exec( |
| 2018 | 2043 | "DROP TABLE IF EXISTS temp.wantalert;" |
| 2019 | - "CREATE TEMP TABLE wantalert(eventId TEXT);" | |
| 2044 | + "CREATE TEMP TABLE wantalert(eventId TEXT, needMod BOOLEAN, sentMod);" | |
| 2020 | 2045 | ); |
| 2021 | 2046 | if( flags & SENDALERT_DIGEST ){ |
| 2047 | + /* Unmoderated changes are never sent as part of a digest */ | |
| 2022 | 2048 | db_multi_exec( |
| 2023 | - "INSERT INTO wantalert SELECT eventid FROM pending_alert" | |
| 2049 | + "INSERT INTO wantalert(eventId,needMod)" | |
| 2050 | + " SELECT eventid, 0" | |
| 2051 | + " FROM pending_alert" | |
| 2024 | 2052 | " WHERE sentDigest IS FALSE" |
| 2053 | + " AND NOT EXISTS(SELECT 1 FROM private WHERE rid=substr(eventid,2));" | |
| 2025 | 2054 | ); |
| 2026 | - zDigest = "true"; | |
| 2027 | 2055 | }else{ |
| 2056 | + /* Immediate alerts might include events that are subject to | |
| 2057 | + ** moderator approval */ | |
| 2028 | 2058 | db_multi_exec( |
| 2029 | - "INSERT INTO wantalert SELECT eventid FROM pending_alert" | |
| 2030 | - " WHERE sentSep IS FALSE" | |
| 2059 | + "INSERT INTO wantalert(eventId,needMod,sentMod)" | |
| 2060 | + " SELECT eventid," | |
| 2061 | + " EXISTS(SELECT 1 FROM private WHERE rid=substr(eventid,2))," | |
| 2062 | + " sentMod" | |
| 2063 | + " FROM pending_alert" | |
| 2064 | + " WHERE sentSep IS FALSE;" | |
| 2065 | + "DELETE FROM wantalert WHERE needMod AND sentMod;" | |
| 2031 | 2066 | ); |
| 2032 | 2067 | } |
| 2033 | - pEvents = email_compute_event_text(&nEvent); | |
| 2068 | + pEvents = email_compute_event_text(&nEvent, (flags & SENDALERT_DIGEST)!=0); | |
| 2034 | 2069 | if( nEvent==0 ) goto send_alerts_done; |
| 2035 | 2070 | blob_init(&hdr, 0, 0); |
| 2036 | 2071 | blob_init(&body, 0, 0); |
| 2037 | 2072 | db_prepare(&q, |
| 2038 | 2073 | "SELECT" |
| @@ -2051,13 +2086,26 @@ | ||
| 2051 | 2086 | const char *zEmail = db_column_text(&q, 1); |
| 2052 | 2087 | const char *zCap = db_column_text(&q, 3); |
| 2053 | 2088 | int nHit = 0; |
| 2054 | 2089 | for(p=pEvents; p; p=p->pNext){ |
| 2055 | 2090 | if( strchr(zSub,p->type)==0 ) continue; |
| 2056 | - if( strchr(zCap,'s')!=0 || strchr(zCap,'a')!=0 ){ | |
| 2057 | - /* Setup and admin users can get any notification */ | |
| 2091 | + if( p->needMod ){ | |
| 2092 | + /* For events that require moderator approval, only send an alert | |
| 2093 | + ** if the recipient is a moderator for that type of event */ | |
| 2094 | + char xType = '*'; | |
| 2095 | + switch( p->type ){ | |
| 2096 | + case 'f': xType = '5'; break; | |
| 2097 | + case 't': xType = 'q'; break; | |
| 2098 | + case 'w': xType = 'l'; break; | |
| 2099 | + } | |
| 2100 | + if( strchr(zCap,xType)==0 ) continue; | |
| 2101 | + }else if( strchr(zCap,'s')!=0 || strchr(zCap,'a')!=0 ){ | |
| 2102 | + /* Setup and admin users can get any notification that does not | |
| 2103 | + ** require moderation */ | |
| 2058 | 2104 | }else{ |
| 2105 | + /* Other users only see the alert if they have sufficient | |
| 2106 | + ** privilege to view the event itself */ | |
| 2059 | 2107 | char xType = '*'; |
| 2060 | 2108 | switch( p->type ){ |
| 2061 | 2109 | case 'c': xType = 'o'; break; |
| 2062 | 2110 | case 'f': xType = '2'; break; |
| 2063 | 2111 | case 't': xType = 'r'; break; |
| @@ -2089,15 +2137,24 @@ | ||
| 2089 | 2137 | blob_reset(&body); |
| 2090 | 2138 | db_finalize(&q); |
| 2091 | 2139 | email_free_eventlist(pEvents); |
| 2092 | 2140 | if( (flags & SENDALERT_PRESERVE)==0 ){ |
| 2093 | 2141 | if( flags & SENDALERT_DIGEST ){ |
| 2094 | - db_multi_exec("UPDATE pending_alert SET sentDigest=true"); | |
| 2142 | + db_multi_exec( | |
| 2143 | + "UPDATE pending_alert SET sentDigest=true" | |
| 2144 | + " WHERE eventid IN (SELECT eventid FROM wantalert);" | |
| 2145 | + "DELETE FROM pending_alert WHERE sentDigest AND sentSep;" | |
| 2146 | + ); | |
| 2095 | 2147 | }else{ |
| 2096 | - db_multi_exec("UPDATE pending_alert SET sentSep=true"); | |
| 2148 | + db_multi_exec( | |
| 2149 | + "UPDATE pending_alert SET sentSep=true" | |
| 2150 | + " WHERE eventid IN (SELECT eventid FROM wantalert WHERE NOT needMod);" | |
| 2151 | + "UPDATE pending_alert SET sentMod=true" | |
| 2152 | + " WHERE eventid IN (SELECT eventid FROM wantalert WHERE needMod);" | |
| 2153 | + "DELETE FROM pending_alert WHERE sentDigest AND sentSep;" | |
| 2154 | + ); | |
| 2097 | 2155 | } |
| 2098 | - db_multi_exec("DELETE FROM pending_alert WHERE sentDigest AND sentSep"); | |
| 2099 | 2156 | } |
| 2100 | 2157 | send_alerts_done: |
| 2101 | 2158 | email_sender_free(pSender); |
| 2102 | 2159 | if( g.fSqlTrace ) fossil_trace("-- END email_send_alerts(%u)\n", flags); |
| 2103 | 2160 | db_end_transaction(0); |
| 2104 | 2161 |
| --- src/email.c | |
| +++ src/email.c | |
| @@ -77,12 +77,13 @@ | |
| 77 | @ -- Remaining characters determine the specific event. For example, |
| 78 | @ -- 'c4413' means check-in with rid=4413. |
| 79 | @ -- |
| 80 | @ CREATE TABLE repository.pending_alert( |
| 81 | @ eventid TEXT PRIMARY KEY, -- Object that changed |
| 82 | @ sentSep BOOLEAN DEFAULT false, -- individual emails sent |
| 83 | @ sentDigest BOOLEAN DEFAULT false -- digest emails sent |
| 84 | @ ) WITHOUT ROWID; |
| 85 | @ |
| 86 | @ DROP TABLE IF EXISTS repository.email_bounce; |
| 87 | @ -- Record bounced emails. If too many bounces are received within |
| 88 | @ -- some defined time range, then cancel the subscription. Older |
| @@ -115,10 +116,15 @@ | |
| 115 | ){ |
| 116 | return; /* Don't create table for disabled email */ |
| 117 | } |
| 118 | db_multi_exec(zEmailInit/*works-like:""*/); |
| 119 | email_triggers_enable(); |
| 120 | } |
| 121 | } |
| 122 | |
| 123 | /* |
| 124 | ** Enable triggers that automatically populate the pending_alert |
| @@ -294,18 +300,10 @@ | |
| 294 | @ <p>This is the email for the human administrator for the system. |
| 295 | @ Abuse and trouble reports are send here. |
| 296 | @ (Property: "email-admin")</p> |
| 297 | @ <hr> |
| 298 | |
| 299 | entry_attribute("Inbound email directory", 40, "email-receive-dir", |
| 300 | "erdir", "", 0); |
| 301 | @ <p>Inbound emails can be stored in a directory for analysis as |
| 302 | @ a debugging aid. Put the name of that directory in this entry box. |
| 303 | @ Disable saving of inbound email by making this an empty string. |
| 304 | @ Abuse and trouble reports are send here. |
| 305 | @ (Property: "email-receive-dir")</p> |
| 306 | @ <hr> |
| 307 | @ <p><input type="submit" name="submit" value="Apply Changes" /></p> |
| 308 | @ </div></form> |
| 309 | db_end_transaction(0); |
| 310 | style_footer(); |
| 311 | } |
| @@ -757,24 +755,10 @@ | |
| 757 | fossil_print("%s", blob_str(&all)); |
| 758 | } |
| 759 | blob_reset(&all); |
| 760 | } |
| 761 | |
| 762 | /* |
| 763 | ** Analyze and act on a received email. |
| 764 | ** |
| 765 | ** This routine takes ownership of the Blob parameter and is responsible |
| 766 | ** for freeing that blob when it is done with it. |
| 767 | ** |
| 768 | ** This routine acts on all email messages received from the |
| 769 | ** "fossil email inbound" command. |
| 770 | */ |
| 771 | void email_receive(Blob *pMsg){ |
| 772 | /* To Do: Look for bounce messages and possibly disable subscriptions */ |
| 773 | blob_reset(pMsg); |
| 774 | } |
| 775 | |
| 776 | /* |
| 777 | ** SETTING: email-send-method width=5 default=off |
| 778 | ** Determine the method used to send email. Allowed values are |
| 779 | ** "off", "relay", "pipe", "dir", "db", and "stdout". The "off" value |
| 780 | ** means no email is ever sent. The "relay" value means emails are sent |
| @@ -806,16 +790,10 @@ | |
| 806 | /* |
| 807 | ** SETTING: email-self width=40 |
| 808 | ** This is the email address for the repository. Outbound emails add |
| 809 | ** this email address as the "From:" field. |
| 810 | */ |
| 811 | /* |
| 812 | ** SETTING: email-receive-dir width=40 |
| 813 | ** Inbound email messages are saved as separate files in this directory, |
| 814 | ** for debugging analysis. Disable saving of inbound emails omitting |
| 815 | ** this setting, or making it an empty string. |
| 816 | */ |
| 817 | /* |
| 818 | ** SETTING: email-send-relayhost width=40 |
| 819 | ** This is the hostname and TCP port to which output email messages |
| 820 | ** are sent when email-send-method is "relay". There should be an |
| 821 | ** SMTP server configured as a Mail Submission Agent listening on the |
| @@ -822,13 +800,13 @@ | |
| 822 | ** designated host and port and all times. |
| 823 | */ |
| 824 | |
| 825 | |
| 826 | /* |
| 827 | ** COMMAND: email |
| 828 | ** |
| 829 | ** Usage: %fossil email SUBCOMMAND ARGS... |
| 830 | ** |
| 831 | ** Subcommands: |
| 832 | ** |
| 833 | ** exec Compose and send pending email alerts. |
| 834 | ** Some installations may want to do this via |
| @@ -837,13 +815,11 @@ | |
| 837 | ** Options: |
| 838 | ** |
| 839 | ** --digest Send digests |
| 840 | ** --test Resets to standard output |
| 841 | ** |
| 842 | ** inbound [FILE] Receive an inbound email message. This message |
| 843 | ** is analyzed to see if it is a bounce, and if |
| 844 | ** necessary, subscribers may be disabled. |
| 845 | ** |
| 846 | ** reset Hard reset of all email notification tables |
| 847 | ** in the repository. This erases all subscription |
| 848 | ** information. Use with extreme care. |
| 849 | ** |
| @@ -857,10 +833,13 @@ | |
| 857 | ** --stdout |
| 858 | ** --subject|-S SUBJECT |
| 859 | ** |
| 860 | ** settings [NAME VALUE] With no arguments, list all email settings. |
| 861 | ** Or change the value of a single email setting. |
| 862 | ** |
| 863 | ** subscribers [PATTERN] List all subscribers matching PATTERN. |
| 864 | ** |
| 865 | ** unsubscribe EMAIL Remove a single subscriber with the given EMAIL. |
| 866 | */ |
| @@ -878,24 +857,24 @@ | |
| 878 | eFlags |= SENDALERT_PRESERVE|SENDALERT_STDOUT; |
| 879 | } |
| 880 | verify_all_options(); |
| 881 | email_send_alerts(eFlags); |
| 882 | }else |
| 883 | if( strncmp(zCmd, "inbound", nCmd)==0 ){ |
| 884 | Blob email; |
| 885 | const char *zInboundDir = db_get("email-receive-dir",""); |
| 886 | verify_all_options(); |
| 887 | if( g.argc!=3 && g.argc!=4 ){ |
| 888 | usage("inbound [FILE]"); |
| 889 | } |
| 890 | blob_read_from_file(&email, g.argc==3 ? "-" : g.argv[3], ExtFILE); |
| 891 | if( zInboundDir[0] ){ |
| 892 | char *zFN = file_time_tempname(zInboundDir,".email"); |
| 893 | blob_write_to_file(&email, zFN); |
| 894 | fossil_free(zFN); |
| 895 | } |
| 896 | email_receive(&email); |
| 897 | }else |
| 898 | if( strncmp(zCmd, "reset", nCmd)==0 ){ |
| 899 | int c; |
| 900 | int bForce = find_option("force","f",0)!=0; |
| 901 | verify_all_options(); |
| @@ -979,10 +958,33 @@ | |
| 979 | for(; nSetting>0; nSetting--, pSetting++ ){ |
| 980 | if( strncmp(pSetting->name,"email-",6)!=0 ) continue; |
| 981 | print_setting(pSetting); |
| 982 | } |
| 983 | }else |
| 984 | if( strncmp(zCmd, "subscribers", nCmd)==0 ){ |
| 985 | Stmt q; |
| 986 | verify_all_options(); |
| 987 | if( g.argc!=3 && g.argc!=4 ) usage("subscribers [PATTERN]"); |
| 988 | if( g.argc==4 ){ |
| @@ -1008,11 +1010,11 @@ | |
| 1008 | if( g.argc!=4 ) usage("unsubscribe EMAIL"); |
| 1009 | db_multi_exec( |
| 1010 | "DELETE FROM subscriber WHERE semail=%Q", g.argv[3]); |
| 1011 | }else |
| 1012 | { |
| 1013 | usage("exec|inbound|reset|send|setting|subscribers|unsubscribe"); |
| 1014 | } |
| 1015 | } |
| 1016 | |
| 1017 | /* |
| 1018 | ** Do error checking on a submitted subscription form. Return TRUE |
| @@ -1789,11 +1791,12 @@ | |
| 1789 | /* |
| 1790 | ** A single event that might appear in an alert is recorded as an |
| 1791 | ** instance of the following object. |
| 1792 | */ |
| 1793 | struct EmailEvent { |
| 1794 | int type; /* 'c', 't', 'w', 'f' */ |
| 1795 | Blob txt; /* Text description to appear in an alert */ |
| 1796 | EmailEvent *pNext; /* Next in chronological order */ |
| 1797 | }; |
| 1798 | #endif |
| 1799 | |
| @@ -1812,33 +1815,34 @@ | |
| 1812 | /* |
| 1813 | ** Compute and return a linked list of EmailEvent objects |
| 1814 | ** corresponding to the current content of the temp.wantalert |
| 1815 | ** table which should be defined as follows: |
| 1816 | ** |
| 1817 | ** CREATE TEMP TABLE wantalert(eventId TEXT); |
| 1818 | */ |
| 1819 | EmailEvent *email_compute_event_text(int *pnEvent){ |
| 1820 | Stmt q; |
| 1821 | EmailEvent *p; |
| 1822 | EmailEvent anchor; |
| 1823 | EmailEvent *pLast; |
| 1824 | const char *zUrl = db_get("email-url","http://localhost:8080"); |
| 1825 | |
| 1826 | db_prepare(&q, |
| 1827 | "SELECT" |
| 1828 | " blob.uuid," /* 0 */ |
| 1829 | " datetime(event.mtime)," /* 1 */ |
| 1830 | " coalesce(ecomment,comment)" |
| 1831 | " || ' (user: ' || coalesce(euser,user,'?')" |
| 1832 | " || (SELECT case when length(x)>0 then ' tags: ' || x else '' end" |
| 1833 | " FROM (SELECT group_concat(substr(tagname,5), ', ') AS x" |
| 1834 | " FROM tag, tagxref" |
| 1835 | " WHERE tagname GLOB 'sym-*' AND tag.tagid=tagxref.tagid" |
| 1836 | " AND tagxref.rid=blob.rid AND tagxref.tagtype>0))" |
| 1837 | " || ')' as comment," /* 2 */ |
| 1838 | " tagxref.value AS branch," /* 3 */ |
| 1839 | " wantalert.eventId" /* 4 */ |
| 1840 | " FROM temp.wantalert JOIN tag CROSS JOIN event CROSS JOIN blob" |
| 1841 | " LEFT JOIN tagxref ON tagxref.tagid=tag.tagid" |
| 1842 | " AND tagxref.tagtype>0" |
| 1843 | " AND tagxref.rid=blob.rid" |
| 1844 | " WHERE blob.rid=event.objid" |
| @@ -1853,13 +1857,15 @@ | |
| 1853 | const char *zType = ""; |
| 1854 | p = fossil_malloc( sizeof(EmailEvent) ); |
| 1855 | pLast->pNext = p; |
| 1856 | pLast = p; |
| 1857 | p->type = db_column_text(&q, 4)[0]; |
| 1858 | p->pNext = 0; |
| 1859 | switch( p->type ){ |
| 1860 | case 'c': zType = "Check-In"; break; |
| 1861 | case 't': zType = "Wiki Edit"; break; |
| 1862 | case 'w': zType = "Ticket Change"; break; |
| 1863 | } |
| 1864 | blob_init(&p->txt, 0, 0); |
| 1865 | blob_appendf(&p->txt,"== %s %s ==\n%s\n%s/info/%.20s\n", |
| @@ -1867,10 +1873,16 @@ | |
| 1867 | zType, |
| 1868 | db_column_text(&q,2), |
| 1869 | zUrl, |
| 1870 | db_column_text(&q,0) |
| 1871 | ); |
| 1872 | (*pnEvent)++; |
| 1873 | } |
| 1874 | db_finalize(&q); |
| 1875 | return anchor.pNext; |
| 1876 | } |
| @@ -1905,32 +1917,44 @@ | |
| 1905 | ** command line, generate text for all events named in the |
| 1906 | ** pending_alert table. |
| 1907 | ** |
| 1908 | ** This command is intended for testing and debugging the logic |
| 1909 | ** that generates email alert text. |
| 1910 | */ |
| 1911 | void test_alert_cmd(void){ |
| 1912 | Blob out; |
| 1913 | int nEvent; |
| 1914 | EmailEvent *pEvent, *p; |
| 1915 | |
| 1916 | db_find_and_open_repository(0, 0); |
| 1917 | verify_all_options(); |
| 1918 | db_begin_transaction(); |
| 1919 | email_schema(0); |
| 1920 | db_multi_exec("CREATE TEMP TABLE wantalert(eventid TEXT)"); |
| 1921 | if( g.argc==2 ){ |
| 1922 | db_multi_exec("INSERT INTO wantalert SELECT eventid FROM pending_alert"); |
| 1923 | }else{ |
| 1924 | int i; |
| 1925 | for(i=2; i<g.argc; i++){ |
| 1926 | db_multi_exec("INSERT INTO wantalert VALUES(%Q)", g.argv[i]); |
| 1927 | } |
| 1928 | } |
| 1929 | blob_init(&out, 0, 0); |
| 1930 | email_header(&out); |
| 1931 | pEvent = email_compute_event_text(&nEvent); |
| 1932 | for(p=pEvent; p; p=p->pNext){ |
| 1933 | blob_append(&out, "\n", 1); |
| 1934 | blob_append(&out, blob_buffer(&p->txt), blob_size(&p->txt)); |
| 1935 | } |
| 1936 | email_free_eventlist(pEvent); |
| @@ -2001,10 +2025,11 @@ | |
| 2001 | EmailSender *pSender = 0; |
| 2002 | u32 senderFlags = 0; |
| 2003 | |
| 2004 | if( g.fSqlTrace ) fossil_trace("-- BEGIN email_send_alerts(%u)\n", flags); |
| 2005 | db_begin_transaction(); |
| 2006 | if( !email_enabled() ) goto send_alerts_done; |
| 2007 | zUrl = db_get("email-url",0); |
| 2008 | if( zUrl==0 ) goto send_alerts_done; |
| 2009 | zRepoName = db_get("email-subname",0); |
| 2010 | if( zRepoName==0 ) goto send_alerts_done; |
| @@ -2014,25 +2039,35 @@ | |
| 2014 | senderFlags |= EMAIL_TRACE; |
| 2015 | } |
| 2016 | pSender = email_sender_new(zDest, senderFlags); |
| 2017 | db_multi_exec( |
| 2018 | "DROP TABLE IF EXISTS temp.wantalert;" |
| 2019 | "CREATE TEMP TABLE wantalert(eventId TEXT);" |
| 2020 | ); |
| 2021 | if( flags & SENDALERT_DIGEST ){ |
| 2022 | db_multi_exec( |
| 2023 | "INSERT INTO wantalert SELECT eventid FROM pending_alert" |
| 2024 | " WHERE sentDigest IS FALSE" |
| 2025 | ); |
| 2026 | zDigest = "true"; |
| 2027 | }else{ |
| 2028 | db_multi_exec( |
| 2029 | "INSERT INTO wantalert SELECT eventid FROM pending_alert" |
| 2030 | " WHERE sentSep IS FALSE" |
| 2031 | ); |
| 2032 | } |
| 2033 | pEvents = email_compute_event_text(&nEvent); |
| 2034 | if( nEvent==0 ) goto send_alerts_done; |
| 2035 | blob_init(&hdr, 0, 0); |
| 2036 | blob_init(&body, 0, 0); |
| 2037 | db_prepare(&q, |
| 2038 | "SELECT" |
| @@ -2051,13 +2086,26 @@ | |
| 2051 | const char *zEmail = db_column_text(&q, 1); |
| 2052 | const char *zCap = db_column_text(&q, 3); |
| 2053 | int nHit = 0; |
| 2054 | for(p=pEvents; p; p=p->pNext){ |
| 2055 | if( strchr(zSub,p->type)==0 ) continue; |
| 2056 | if( strchr(zCap,'s')!=0 || strchr(zCap,'a')!=0 ){ |
| 2057 | /* Setup and admin users can get any notification */ |
| 2058 | }else{ |
| 2059 | char xType = '*'; |
| 2060 | switch( p->type ){ |
| 2061 | case 'c': xType = 'o'; break; |
| 2062 | case 'f': xType = '2'; break; |
| 2063 | case 't': xType = 'r'; break; |
| @@ -2089,15 +2137,24 @@ | |
| 2089 | blob_reset(&body); |
| 2090 | db_finalize(&q); |
| 2091 | email_free_eventlist(pEvents); |
| 2092 | if( (flags & SENDALERT_PRESERVE)==0 ){ |
| 2093 | if( flags & SENDALERT_DIGEST ){ |
| 2094 | db_multi_exec("UPDATE pending_alert SET sentDigest=true"); |
| 2095 | }else{ |
| 2096 | db_multi_exec("UPDATE pending_alert SET sentSep=true"); |
| 2097 | } |
| 2098 | db_multi_exec("DELETE FROM pending_alert WHERE sentDigest AND sentSep"); |
| 2099 | } |
| 2100 | send_alerts_done: |
| 2101 | email_sender_free(pSender); |
| 2102 | if( g.fSqlTrace ) fossil_trace("-- END email_send_alerts(%u)\n", flags); |
| 2103 | db_end_transaction(0); |
| 2104 |
| --- src/email.c | |
| +++ src/email.c | |
| @@ -77,12 +77,13 @@ | |
| 77 | @ -- Remaining characters determine the specific event. For example, |
| 78 | @ -- 'c4413' means check-in with rid=4413. |
| 79 | @ -- |
| 80 | @ CREATE TABLE repository.pending_alert( |
| 81 | @ eventid TEXT PRIMARY KEY, -- Object that changed |
| 82 | @ sentSep BOOLEAN DEFAULT false, -- individual alert sent |
| 83 | @ sentDigest BOOLEAN DEFAULT false, -- digest alert sent |
| 84 | @ sentMod BOOLEAN DEFAULT false -- pending moderation alert sent |
| 85 | @ ) WITHOUT ROWID; |
| 86 | @ |
| 87 | @ DROP TABLE IF EXISTS repository.email_bounce; |
| 88 | @ -- Record bounced emails. If too many bounces are received within |
| 89 | @ -- some defined time range, then cancel the subscription. Older |
| @@ -115,10 +116,15 @@ | |
| 116 | ){ |
| 117 | return; /* Don't create table for disabled email */ |
| 118 | } |
| 119 | db_multi_exec(zEmailInit/*works-like:""*/); |
| 120 | email_triggers_enable(); |
| 121 | }else if( !db_table_has_column("repository","pending_alert","sentMod") ){ |
| 122 | db_multi_exec( |
| 123 | "ALTER TABLE repository.pending_alert" |
| 124 | " ADD COLUMN sentMod BOOLEAN DEFAULT false;" |
| 125 | ); |
| 126 | } |
| 127 | } |
| 128 | |
| 129 | /* |
| 130 | ** Enable triggers that automatically populate the pending_alert |
| @@ -294,18 +300,10 @@ | |
| 300 | @ <p>This is the email for the human administrator for the system. |
| 301 | @ Abuse and trouble reports are send here. |
| 302 | @ (Property: "email-admin")</p> |
| 303 | @ <hr> |
| 304 | |
| 305 | @ <p><input type="submit" name="submit" value="Apply Changes" /></p> |
| 306 | @ </div></form> |
| 307 | db_end_transaction(0); |
| 308 | style_footer(); |
| 309 | } |
| @@ -757,24 +755,10 @@ | |
| 755 | fossil_print("%s", blob_str(&all)); |
| 756 | } |
| 757 | blob_reset(&all); |
| 758 | } |
| 759 | |
| 760 | /* |
| 761 | ** SETTING: email-send-method width=5 default=off |
| 762 | ** Determine the method used to send email. Allowed values are |
| 763 | ** "off", "relay", "pipe", "dir", "db", and "stdout". The "off" value |
| 764 | ** means no email is ever sent. The "relay" value means emails are sent |
| @@ -806,16 +790,10 @@ | |
| 790 | /* |
| 791 | ** SETTING: email-self width=40 |
| 792 | ** This is the email address for the repository. Outbound emails add |
| 793 | ** this email address as the "From:" field. |
| 794 | */ |
| 795 | /* |
| 796 | ** SETTING: email-send-relayhost width=40 |
| 797 | ** This is the hostname and TCP port to which output email messages |
| 798 | ** are sent when email-send-method is "relay". There should be an |
| 799 | ** SMTP server configured as a Mail Submission Agent listening on the |
| @@ -822,13 +800,13 @@ | |
| 800 | ** designated host and port and all times. |
| 801 | */ |
| 802 | |
| 803 | |
| 804 | /* |
| 805 | ** COMMAND: alerts |
| 806 | ** |
| 807 | ** Usage: %fossil alerts SUBCOMMAND ARGS... |
| 808 | ** |
| 809 | ** Subcommands: |
| 810 | ** |
| 811 | ** exec Compose and send pending email alerts. |
| 812 | ** Some installations may want to do this via |
| @@ -837,13 +815,11 @@ | |
| 815 | ** Options: |
| 816 | ** |
| 817 | ** --digest Send digests |
| 818 | ** --test Resets to standard output |
| 819 | ** |
| 820 | ** pending Show all pending alerts. Useful for debugging. |
| 821 | ** |
| 822 | ** reset Hard reset of all email notification tables |
| 823 | ** in the repository. This erases all subscription |
| 824 | ** information. Use with extreme care. |
| 825 | ** |
| @@ -857,10 +833,13 @@ | |
| 833 | ** --stdout |
| 834 | ** --subject|-S SUBJECT |
| 835 | ** |
| 836 | ** settings [NAME VALUE] With no arguments, list all email settings. |
| 837 | ** Or change the value of a single email setting. |
| 838 | ** |
| 839 | ** status Report on the status of the email alert |
| 840 | ** subsystem |
| 841 | ** |
| 842 | ** subscribers [PATTERN] List all subscribers matching PATTERN. |
| 843 | ** |
| 844 | ** unsubscribe EMAIL Remove a single subscriber with the given EMAIL. |
| 845 | */ |
| @@ -878,24 +857,24 @@ | |
| 857 | eFlags |= SENDALERT_PRESERVE|SENDALERT_STDOUT; |
| 858 | } |
| 859 | verify_all_options(); |
| 860 | email_send_alerts(eFlags); |
| 861 | }else |
| 862 | if( strncmp(zCmd, "pending", nCmd)==0 ){ |
| 863 | Stmt q; |
| 864 | verify_all_options(); |
| 865 | if( g.argc!=3 ) usage("pending"); |
| 866 | db_prepare(&q,"SELECT eventid, sentSep, sentDigest, sentMod" |
| 867 | " FROM pending_alert"); |
| 868 | while( db_step(&q)==SQLITE_ROW ){ |
| 869 | fossil_print("%10s %7s %10s %7s\n", |
| 870 | db_column_text(&q,0), |
| 871 | db_column_int(&q,1) ? "sentSep" : "", |
| 872 | db_column_int(&q,2) ? "sentDigest" : "", |
| 873 | db_column_int(&q,3) ? "sentMod" : ""); |
| 874 | } |
| 875 | db_finalize(&q); |
| 876 | }else |
| 877 | if( strncmp(zCmd, "reset", nCmd)==0 ){ |
| 878 | int c; |
| 879 | int bForce = find_option("force","f",0)!=0; |
| 880 | verify_all_options(); |
| @@ -979,10 +958,33 @@ | |
| 958 | for(; nSetting>0; nSetting--, pSetting++ ){ |
| 959 | if( strncmp(pSetting->name,"email-",6)!=0 ) continue; |
| 960 | print_setting(pSetting); |
| 961 | } |
| 962 | }else |
| 963 | if( strncmp(zCmd, "status", nCmd)==0 ){ |
| 964 | int isGlobal = find_option("global",0,0)!=0; |
| 965 | int nSetting, n; |
| 966 | static const char *zFmt = "%-29s %d\n"; |
| 967 | const Setting *pSetting = setting_info(&nSetting); |
| 968 | db_open_config(1, 0); |
| 969 | verify_all_options(); |
| 970 | if( g.argc!=3 ) usage("status"); |
| 971 | pSetting = setting_info(&nSetting); |
| 972 | for(; nSetting>0; nSetting--, pSetting++ ){ |
| 973 | if( strncmp(pSetting->name,"email-",6)!=0 ) continue; |
| 974 | print_setting(pSetting); |
| 975 | } |
| 976 | n = db_int(0,"SELECT count(*) FROM pending_alert WHERE NOT sentSep"); |
| 977 | fossil_print(zFmt/*works-like:"%s%d"*/, "pending-alerts", n); |
| 978 | n = db_int(0,"SELECT count(*) FROM pending_alert WHERE NOT sentDigest"); |
| 979 | fossil_print(zFmt/*works-like:"%s%d"*/, "pending-digest-alerts", n); |
| 980 | n = db_int(0,"SELECT count(*) FROM subscriber"); |
| 981 | fossil_print(zFmt/*works-like:"%s%d"*/, "total-subscribers", n); |
| 982 | n = db_int(0, "SELECT count(*) FROM subscriber WHERE sverified" |
| 983 | " AND NOT sdonotcall AND length(ssub)>1"); |
| 984 | fossil_print(zFmt/*works-like:"%s%d"*/, "active-subscribers", n); |
| 985 | }else |
| 986 | if( strncmp(zCmd, "subscribers", nCmd)==0 ){ |
| 987 | Stmt q; |
| 988 | verify_all_options(); |
| 989 | if( g.argc!=3 && g.argc!=4 ) usage("subscribers [PATTERN]"); |
| 990 | if( g.argc==4 ){ |
| @@ -1008,11 +1010,11 @@ | |
| 1010 | if( g.argc!=4 ) usage("unsubscribe EMAIL"); |
| 1011 | db_multi_exec( |
| 1012 | "DELETE FROM subscriber WHERE semail=%Q", g.argv[3]); |
| 1013 | }else |
| 1014 | { |
| 1015 | usage("exec|pending|reset|send|setting|status|subscribers|unsubscribe"); |
| 1016 | } |
| 1017 | } |
| 1018 | |
| 1019 | /* |
| 1020 | ** Do error checking on a submitted subscription form. Return TRUE |
| @@ -1789,11 +1791,12 @@ | |
| 1791 | /* |
| 1792 | ** A single event that might appear in an alert is recorded as an |
| 1793 | ** instance of the following object. |
| 1794 | */ |
| 1795 | struct EmailEvent { |
| 1796 | int type; /* 'c', 'f', 'm', 't', 'w' */ |
| 1797 | int needMod; /* Pending moderator approval */ |
| 1798 | Blob txt; /* Text description to appear in an alert */ |
| 1799 | EmailEvent *pNext; /* Next in chronological order */ |
| 1800 | }; |
| 1801 | #endif |
| 1802 | |
| @@ -1812,33 +1815,34 @@ | |
| 1815 | /* |
| 1816 | ** Compute and return a linked list of EmailEvent objects |
| 1817 | ** corresponding to the current content of the temp.wantalert |
| 1818 | ** table which should be defined as follows: |
| 1819 | ** |
| 1820 | ** CREATE TEMP TABLE wantalert(eventId TEXT, needMod BOOLEAN); |
| 1821 | */ |
| 1822 | EmailEvent *email_compute_event_text(int *pnEvent, int doDigest){ |
| 1823 | Stmt q; |
| 1824 | EmailEvent *p; |
| 1825 | EmailEvent anchor; |
| 1826 | EmailEvent *pLast; |
| 1827 | const char *zUrl = db_get("email-url","http://localhost:8080"); |
| 1828 | |
| 1829 | db_prepare(&q, |
| 1830 | "SELECT" |
| 1831 | " blob.uuid," /* 0 */ |
| 1832 | " datetime(event.mtime)," /* 1 */ |
| 1833 | " coalesce(ecomment,comment)" |
| 1834 | " || ' (user: ' || coalesce(euser,user,'?')" |
| 1835 | " || (SELECT case when length(x)>0 then ' tags: ' || x else '' end" |
| 1836 | " FROM (SELECT group_concat(substr(tagname,5), ', ') AS x" |
| 1837 | " FROM tag, tagxref" |
| 1838 | " WHERE tagname GLOB 'sym-*' AND tag.tagid=tagxref.tagid" |
| 1839 | " AND tagxref.rid=blob.rid AND tagxref.tagtype>0))" |
| 1840 | " || ')' as comment," /* 2 */ |
| 1841 | " tagxref.value AS branch," /* 3 */ |
| 1842 | " wantalert.eventId," /* 4 */ |
| 1843 | " wantalert.needMod" /* 5 */ |
| 1844 | " FROM temp.wantalert JOIN tag CROSS JOIN event CROSS JOIN blob" |
| 1845 | " LEFT JOIN tagxref ON tagxref.tagid=tag.tagid" |
| 1846 | " AND tagxref.tagtype>0" |
| 1847 | " AND tagxref.rid=blob.rid" |
| 1848 | " WHERE blob.rid=event.objid" |
| @@ -1853,13 +1857,15 @@ | |
| 1857 | const char *zType = ""; |
| 1858 | p = fossil_malloc( sizeof(EmailEvent) ); |
| 1859 | pLast->pNext = p; |
| 1860 | pLast = p; |
| 1861 | p->type = db_column_text(&q, 4)[0]; |
| 1862 | p->needMod = db_column_int(&q, 5); |
| 1863 | p->pNext = 0; |
| 1864 | switch( p->type ){ |
| 1865 | case 'c': zType = "Check-In"; break; |
| 1866 | case 'f': zType = "Forum post"; break; |
| 1867 | case 't': zType = "Wiki Edit"; break; |
| 1868 | case 'w': zType = "Ticket Change"; break; |
| 1869 | } |
| 1870 | blob_init(&p->txt, 0, 0); |
| 1871 | blob_appendf(&p->txt,"== %s %s ==\n%s\n%s/info/%.20s\n", |
| @@ -1867,10 +1873,16 @@ | |
| 1873 | zType, |
| 1874 | db_column_text(&q,2), |
| 1875 | zUrl, |
| 1876 | db_column_text(&q,0) |
| 1877 | ); |
| 1878 | if( p->needMod ){ |
| 1879 | blob_appendf(&p->txt, |
| 1880 | "** Pending moderator approval (%s/modreq) **\n", |
| 1881 | zUrl |
| 1882 | ); |
| 1883 | } |
| 1884 | (*pnEvent)++; |
| 1885 | } |
| 1886 | db_finalize(&q); |
| 1887 | return anchor.pNext; |
| 1888 | } |
| @@ -1905,32 +1917,44 @@ | |
| 1917 | ** command line, generate text for all events named in the |
| 1918 | ** pending_alert table. |
| 1919 | ** |
| 1920 | ** This command is intended for testing and debugging the logic |
| 1921 | ** that generates email alert text. |
| 1922 | ** |
| 1923 | ** Options: |
| 1924 | ** |
| 1925 | ** --digest Generate digest alert text |
| 1926 | ** --needmod Assume all events are pending moderator approval |
| 1927 | */ |
| 1928 | void test_alert_cmd(void){ |
| 1929 | Blob out; |
| 1930 | int nEvent; |
| 1931 | int needMod; |
| 1932 | int doDigest; |
| 1933 | EmailEvent *pEvent, *p; |
| 1934 | |
| 1935 | doDigest = find_option("digest",0,0)!=0; |
| 1936 | needMod = find_option("needmod",0,0)!=0; |
| 1937 | db_find_and_open_repository(0, 0); |
| 1938 | verify_all_options(); |
| 1939 | db_begin_transaction(); |
| 1940 | email_schema(0); |
| 1941 | db_multi_exec("CREATE TEMP TABLE wantalert(eventid TEXT, needMod BOOLEAN)"); |
| 1942 | if( g.argc==2 ){ |
| 1943 | db_multi_exec( |
| 1944 | "INSERT INTO wantalert(eventId,needMod)" |
| 1945 | " SELECT eventid, %d FROM pending_alert", needMod); |
| 1946 | }else{ |
| 1947 | int i; |
| 1948 | for(i=2; i<g.argc; i++){ |
| 1949 | db_multi_exec("INSERT INTO wantalert(eventId,needMod) VALUES(%Q,%d)", |
| 1950 | g.argv[i], needMod); |
| 1951 | } |
| 1952 | } |
| 1953 | blob_init(&out, 0, 0); |
| 1954 | email_header(&out); |
| 1955 | pEvent = email_compute_event_text(&nEvent, doDigest); |
| 1956 | for(p=pEvent; p; p=p->pNext){ |
| 1957 | blob_append(&out, "\n", 1); |
| 1958 | blob_append(&out, blob_buffer(&p->txt), blob_size(&p->txt)); |
| 1959 | } |
| 1960 | email_free_eventlist(pEvent); |
| @@ -2001,10 +2025,11 @@ | |
| 2025 | EmailSender *pSender = 0; |
| 2026 | u32 senderFlags = 0; |
| 2027 | |
| 2028 | if( g.fSqlTrace ) fossil_trace("-- BEGIN email_send_alerts(%u)\n", flags); |
| 2029 | db_begin_transaction(); |
| 2030 | email_schema(0); |
| 2031 | if( !email_enabled() ) goto send_alerts_done; |
| 2032 | zUrl = db_get("email-url",0); |
| 2033 | if( zUrl==0 ) goto send_alerts_done; |
| 2034 | zRepoName = db_get("email-subname",0); |
| 2035 | if( zRepoName==0 ) goto send_alerts_done; |
| @@ -2014,25 +2039,35 @@ | |
| 2039 | senderFlags |= EMAIL_TRACE; |
| 2040 | } |
| 2041 | pSender = email_sender_new(zDest, senderFlags); |
| 2042 | db_multi_exec( |
| 2043 | "DROP TABLE IF EXISTS temp.wantalert;" |
| 2044 | "CREATE TEMP TABLE wantalert(eventId TEXT, needMod BOOLEAN, sentMod);" |
| 2045 | ); |
| 2046 | if( flags & SENDALERT_DIGEST ){ |
| 2047 | /* Unmoderated changes are never sent as part of a digest */ |
| 2048 | db_multi_exec( |
| 2049 | "INSERT INTO wantalert(eventId,needMod)" |
| 2050 | " SELECT eventid, 0" |
| 2051 | " FROM pending_alert" |
| 2052 | " WHERE sentDigest IS FALSE" |
| 2053 | " AND NOT EXISTS(SELECT 1 FROM private WHERE rid=substr(eventid,2));" |
| 2054 | ); |
| 2055 | }else{ |
| 2056 | /* Immediate alerts might include events that are subject to |
| 2057 | ** moderator approval */ |
| 2058 | db_multi_exec( |
| 2059 | "INSERT INTO wantalert(eventId,needMod,sentMod)" |
| 2060 | " SELECT eventid," |
| 2061 | " EXISTS(SELECT 1 FROM private WHERE rid=substr(eventid,2))," |
| 2062 | " sentMod" |
| 2063 | " FROM pending_alert" |
| 2064 | " WHERE sentSep IS FALSE;" |
| 2065 | "DELETE FROM wantalert WHERE needMod AND sentMod;" |
| 2066 | ); |
| 2067 | } |
| 2068 | pEvents = email_compute_event_text(&nEvent, (flags & SENDALERT_DIGEST)!=0); |
| 2069 | if( nEvent==0 ) goto send_alerts_done; |
| 2070 | blob_init(&hdr, 0, 0); |
| 2071 | blob_init(&body, 0, 0); |
| 2072 | db_prepare(&q, |
| 2073 | "SELECT" |
| @@ -2051,13 +2086,26 @@ | |
| 2086 | const char *zEmail = db_column_text(&q, 1); |
| 2087 | const char *zCap = db_column_text(&q, 3); |
| 2088 | int nHit = 0; |
| 2089 | for(p=pEvents; p; p=p->pNext){ |
| 2090 | if( strchr(zSub,p->type)==0 ) continue; |
| 2091 | if( p->needMod ){ |
| 2092 | /* For events that require moderator approval, only send an alert |
| 2093 | ** if the recipient is a moderator for that type of event */ |
| 2094 | char xType = '*'; |
| 2095 | switch( p->type ){ |
| 2096 | case 'f': xType = '5'; break; |
| 2097 | case 't': xType = 'q'; break; |
| 2098 | case 'w': xType = 'l'; break; |
| 2099 | } |
| 2100 | if( strchr(zCap,xType)==0 ) continue; |
| 2101 | }else if( strchr(zCap,'s')!=0 || strchr(zCap,'a')!=0 ){ |
| 2102 | /* Setup and admin users can get any notification that does not |
| 2103 | ** require moderation */ |
| 2104 | }else{ |
| 2105 | /* Other users only see the alert if they have sufficient |
| 2106 | ** privilege to view the event itself */ |
| 2107 | char xType = '*'; |
| 2108 | switch( p->type ){ |
| 2109 | case 'c': xType = 'o'; break; |
| 2110 | case 'f': xType = '2'; break; |
| 2111 | case 't': xType = 'r'; break; |
| @@ -2089,15 +2137,24 @@ | |
| 2137 | blob_reset(&body); |
| 2138 | db_finalize(&q); |
| 2139 | email_free_eventlist(pEvents); |
| 2140 | if( (flags & SENDALERT_PRESERVE)==0 ){ |
| 2141 | if( flags & SENDALERT_DIGEST ){ |
| 2142 | db_multi_exec( |
| 2143 | "UPDATE pending_alert SET sentDigest=true" |
| 2144 | " WHERE eventid IN (SELECT eventid FROM wantalert);" |
| 2145 | "DELETE FROM pending_alert WHERE sentDigest AND sentSep;" |
| 2146 | ); |
| 2147 | }else{ |
| 2148 | db_multi_exec( |
| 2149 | "UPDATE pending_alert SET sentSep=true" |
| 2150 | " WHERE eventid IN (SELECT eventid FROM wantalert WHERE NOT needMod);" |
| 2151 | "UPDATE pending_alert SET sentMod=true" |
| 2152 | " WHERE eventid IN (SELECT eventid FROM wantalert WHERE needMod);" |
| 2153 | "DELETE FROM pending_alert WHERE sentDigest AND sentSep;" |
| 2154 | ); |
| 2155 | } |
| 2156 | } |
| 2157 | send_alerts_done: |
| 2158 | email_sender_free(pSender); |
| 2159 | if( g.fSqlTrace ) fossil_trace("-- END email_send_alerts(%u)\n", flags); |
| 2160 | db_end_transaction(0); |
| 2161 |