Fossil SCM
Boo. All day I have been checking in changes to the failed-fix branch when I should have been putting them on the forum-v2 branch. This is a cherry-pick merge that moves all of the changes from today from failed-fix over to forum-v2 in one go. The "email" command is renamed to "alert" and is revised for a better interface. Events that are waiting on moderator approval are not shown to non-moderator users.
Commit
3c532ec55bf24a4c175976436cee6b6a61931aa9684c0934e8f1e74cd2008703
Parent
725bf3ba3bc76a6…
2 files changed
+225
-143
+2
-1
+225
-143
| --- 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,80 +800,72 @@ | ||
| 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 | -** exec Compose and send pending email alerts. | |
| 811 | +** pending Show all pending alerts. Useful for debugging. | |
| 812 | +** | |
| 813 | +** reset Hard reset of all email notification tables | |
| 814 | +** in the repository. This erases all subscription | |
| 815 | +** information. ** Use with extreme care ** | |
| 816 | +** | |
| 817 | +** send Compose and send pending email alerts. | |
| 834 | 818 | ** Some installations may want to do this via |
| 835 | 819 | ** a cron-job to make sure alerts are sent |
| 836 | 820 | ** in a timely manner. |
| 837 | 821 | ** Options: |
| 838 | 822 | ** |
| 839 | 823 | ** --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 | -** | |
| 850 | -** send TO [OPTIONS] Send a single email message using whatever | |
| 824 | +** --test Write to standard output | |
| 825 | +** | |
| 826 | +** settings [NAME VALUE] With no arguments, list all email settings. | |
| 827 | +** Or change the value of a single email setting. | |
| 828 | +** | |
| 829 | +** status Report on the status of the email alert | |
| 830 | +** subsystem | |
| 831 | +** | |
| 832 | +** subscribers [PATTERN] List all subscribers matching PATTERN. | |
| 833 | +** | |
| 834 | +** test-message TO [OPTS] Send a single email message using whatever | |
| 851 | 835 | ** email sending mechanism is currently configured. |
| 852 | -** Use this for testing the email configuration. | |
| 853 | -** Options: | |
| 836 | +** Use this for testing the email notification | |
| 837 | +** configuration. Options: | |
| 854 | 838 | ** |
| 855 | 839 | ** --body FILENAME |
| 856 | 840 | ** --smtp-trace |
| 857 | 841 | ** --stdout |
| 858 | 842 | ** --subject|-S SUBJECT |
| 859 | 843 | ** |
| 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 | 844 | ** unsubscribe EMAIL Remove a single subscriber with the given EMAIL. |
| 866 | 845 | */ |
| 867 | 846 | void email_cmd(void){ |
| 868 | 847 | const char *zCmd; |
| 869 | 848 | int nCmd; |
| 870 | 849 | db_find_and_open_repository(0, 0); |
| 871 | 850 | email_schema(0); |
| 872 | 851 | zCmd = g.argc>=3 ? g.argv[2] : "x"; |
| 873 | 852 | nCmd = (int)strlen(zCmd); |
| 874 | - if( strncmp(zCmd, "exec", nCmd)==0 ){ | |
| 875 | - u32 eFlags = 0; | |
| 876 | - if( find_option("digest",0,0)!=0 ) eFlags |= SENDALERT_DIGEST; | |
| 877 | - if( find_option("test",0,0)!=0 ){ | |
| 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); | |
| 853 | + if( strncmp(zCmd, "pending", nCmd)==0 ){ | |
| 854 | + Stmt q; | |
| 855 | + verify_all_options(); | |
| 856 | + if( g.argc!=3 ) usage("pending"); | |
| 857 | + db_prepare(&q,"SELECT eventid, sentSep, sentDigest, sentMod" | |
| 858 | + " FROM pending_alert"); | |
| 859 | + while( db_step(&q)==SQLITE_ROW ){ | |
| 860 | + fossil_print("%10s %7s %10s %7s\n", | |
| 861 | + db_column_text(&q,0), | |
| 862 | + db_column_int(&q,1) ? "sentSep" : "", | |
| 863 | + db_column_int(&q,2) ? "sentDigest" : "", | |
| 864 | + db_column_int(&q,3) ? "sentMod" : ""); | |
| 865 | + } | |
| 866 | + db_finalize(&q); | |
| 897 | 867 | }else |
| 898 | 868 | if( strncmp(zCmd, "reset", nCmd)==0 ){ |
| 899 | 869 | int c; |
| 900 | 870 | int bForce = find_option("force","f",0)!=0; |
| 901 | 871 | verify_all_options(); |
| @@ -923,10 +893,85 @@ | ||
| 923 | 893 | ); |
| 924 | 894 | email_schema(0); |
| 925 | 895 | } |
| 926 | 896 | }else |
| 927 | 897 | if( strncmp(zCmd, "send", nCmd)==0 ){ |
| 898 | + u32 eFlags = 0; | |
| 899 | + if( find_option("digest",0,0)!=0 ) eFlags |= SENDALERT_DIGEST; | |
| 900 | + if( find_option("test",0,0)!=0 ){ | |
| 901 | + eFlags |= SENDALERT_PRESERVE|SENDALERT_STDOUT; | |
| 902 | + } | |
| 903 | + verify_all_options(); | |
| 904 | + email_send_alerts(eFlags); | |
| 905 | + }else | |
| 906 | + if( strncmp(zCmd, "settings", nCmd)==0 ){ | |
| 907 | + int isGlobal = find_option("global",0,0)!=0; | |
| 908 | + int nSetting; | |
| 909 | + const Setting *pSetting = setting_info(&nSetting); | |
| 910 | + db_open_config(1, 0); | |
| 911 | + verify_all_options(); | |
| 912 | + if( g.argc!=3 && g.argc!=5 ) usage("setting [NAME VALUE]"); | |
| 913 | + if( g.argc==5 ){ | |
| 914 | + const char *zLabel = g.argv[3]; | |
| 915 | + if( strncmp(zLabel, "email-", 6)!=0 | |
| 916 | + || (pSetting = db_find_setting(zLabel, 1))==0 ){ | |
| 917 | + fossil_fatal("not a valid email setting: \"%s\"", zLabel); | |
| 918 | + } | |
| 919 | + db_set(pSetting->name, g.argv[4], isGlobal); | |
| 920 | + g.argc = 3; | |
| 921 | + } | |
| 922 | + pSetting = setting_info(&nSetting); | |
| 923 | + for(; nSetting>0; nSetting--, pSetting++ ){ | |
| 924 | + if( strncmp(pSetting->name,"email-",6)!=0 ) continue; | |
| 925 | + print_setting(pSetting); | |
| 926 | + } | |
| 927 | + }else | |
| 928 | + if( strncmp(zCmd, "status", nCmd)==0 ){ | |
| 929 | + int nSetting, n; | |
| 930 | + static const char *zFmt = "%-29s %d\n"; | |
| 931 | + const Setting *pSetting = setting_info(&nSetting); | |
| 932 | + db_open_config(1, 0); | |
| 933 | + verify_all_options(); | |
| 934 | + if( g.argc!=3 ) usage("status"); | |
| 935 | + pSetting = setting_info(&nSetting); | |
| 936 | + for(; nSetting>0; nSetting--, pSetting++ ){ | |
| 937 | + if( strncmp(pSetting->name,"email-",6)!=0 ) continue; | |
| 938 | + print_setting(pSetting); | |
| 939 | + } | |
| 940 | + n = db_int(0,"SELECT count(*) FROM pending_alert WHERE NOT sentSep"); | |
| 941 | + fossil_print(zFmt/*works-like:"%s%d"*/, "pending-alerts", n); | |
| 942 | + n = db_int(0,"SELECT count(*) FROM pending_alert WHERE NOT sentDigest"); | |
| 943 | + fossil_print(zFmt/*works-like:"%s%d"*/, "pending-digest-alerts", n); | |
| 944 | + n = db_int(0,"SELECT count(*) FROM subscriber"); | |
| 945 | + fossil_print(zFmt/*works-like:"%s%d"*/, "total-subscribers", n); | |
| 946 | + n = db_int(0, "SELECT count(*) FROM subscriber WHERE sverified" | |
| 947 | + " AND NOT sdonotcall AND length(ssub)>1"); | |
| 948 | + fossil_print(zFmt/*works-like:"%s%d"*/, "active-subscribers", n); | |
| 949 | + }else | |
| 950 | + if( strncmp(zCmd, "subscribers", nCmd)==0 ){ | |
| 951 | + Stmt q; | |
| 952 | + verify_all_options(); | |
| 953 | + if( g.argc!=3 && g.argc!=4 ) usage("subscribers [PATTERN]"); | |
| 954 | + if( g.argc==4 ){ | |
| 955 | + char *zPattern = g.argv[3]; | |
| 956 | + db_prepare(&q, | |
| 957 | + "SELECT semail FROM subscriber" | |
| 958 | + " WHERE semail LIKE '%%%q%%' OR suname LIKE '%%%q%%'" | |
| 959 | + " OR semail GLOB '*%q*' or suname GLOB '*%q*'" | |
| 960 | + " ORDER BY semail", | |
| 961 | + zPattern, zPattern, zPattern, zPattern); | |
| 962 | + }else{ | |
| 963 | + db_prepare(&q, | |
| 964 | + "SELECT semail FROM subscriber" | |
| 965 | + " ORDER BY semail"); | |
| 966 | + } | |
| 967 | + while( db_step(&q)==SQLITE_ROW ){ | |
| 968 | + fossil_print("%s\n", db_column_text(&q, 0)); | |
| 969 | + } | |
| 970 | + db_finalize(&q); | |
| 971 | + }else | |
| 972 | + if( strncmp(zCmd, "test-message", nCmd)==0 ){ | |
| 928 | 973 | Blob prompt, body, hdr; |
| 929 | 974 | const char *zDest = find_option("stdout",0,0)!=0 ? "stdout" : 0; |
| 930 | 975 | int i; |
| 931 | 976 | u32 mFlags = EMAIL_IMMEDIATE_FAIL; |
| 932 | 977 | const char *zSubject = find_option("subject", "S", 1); |
| @@ -957,62 +1002,19 @@ | ||
| 957 | 1002 | email_sender_free(pSender); |
| 958 | 1003 | blob_reset(&hdr); |
| 959 | 1004 | blob_reset(&body); |
| 960 | 1005 | blob_reset(&prompt); |
| 961 | 1006 | }else |
| 962 | - if( strncmp(zCmd, "settings", nCmd)==0 ){ | |
| 963 | - int isGlobal = find_option("global",0,0)!=0; | |
| 964 | - int nSetting; | |
| 965 | - const Setting *pSetting = setting_info(&nSetting); | |
| 966 | - db_open_config(1, 0); | |
| 967 | - verify_all_options(); | |
| 968 | - if( g.argc!=3 && g.argc!=5 ) usage("setting [NAME VALUE]"); | |
| 969 | - if( g.argc==5 ){ | |
| 970 | - const char *zLabel = g.argv[3]; | |
| 971 | - if( strncmp(zLabel, "email-", 6)!=0 | |
| 972 | - || (pSetting = db_find_setting(zLabel, 1))==0 ){ | |
| 973 | - fossil_fatal("not a valid email setting: \"%s\"", zLabel); | |
| 974 | - } | |
| 975 | - db_set(pSetting->name, g.argv[4], isGlobal); | |
| 976 | - g.argc = 3; | |
| 977 | - } | |
| 978 | - pSetting = setting_info(&nSetting); | |
| 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 ){ | |
| 989 | - char *zPattern = g.argv[3]; | |
| 990 | - db_prepare(&q, | |
| 991 | - "SELECT semail FROM subscriber" | |
| 992 | - " WHERE semail LIKE '%%%q%%' OR suname LIKE '%%%q%%'" | |
| 993 | - " OR semail GLOB '*%q*' or suname GLOB '*%q*'" | |
| 994 | - " ORDER BY semail", | |
| 995 | - zPattern, zPattern, zPattern, zPattern); | |
| 996 | - }else{ | |
| 997 | - db_prepare(&q, | |
| 998 | - "SELECT semail FROM subscriber" | |
| 999 | - " ORDER BY semail"); | |
| 1000 | - } | |
| 1001 | - while( db_step(&q)==SQLITE_ROW ){ | |
| 1002 | - fossil_print("%s\n", db_column_text(&q, 0)); | |
| 1003 | - } | |
| 1004 | - db_finalize(&q); | |
| 1005 | - }else | |
| 1006 | 1007 | if( strncmp(zCmd, "unsubscribe", nCmd)==0 ){ |
| 1007 | 1008 | verify_all_options(); |
| 1008 | 1009 | if( g.argc!=4 ) usage("unsubscribe EMAIL"); |
| 1009 | 1010 | db_multi_exec( |
| 1010 | 1011 | "DELETE FROM subscriber WHERE semail=%Q", g.argv[3]); |
| 1011 | 1012 | }else |
| 1012 | 1013 | { |
| 1013 | - usage("exec|inbound|reset|send|setting|subscribers|unsubscribe"); | |
| 1014 | + usage("pending|reset|send|setting|status|" | |
| 1015 | + "subscribers|test-message|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); |
| @@ -1947,12 +1971,12 @@ | ||
| 1947 | 1971 | ** |
| 1948 | 1972 | ** Add one or more events to the pending_alert queue. Use this |
| 1949 | 1973 | ** command during testing to force email notifications for specific |
| 1950 | 1974 | ** events. |
| 1951 | 1975 | ** |
| 1952 | -** EVENTIDs are text. The first character is 'c', 'w', or 't' | |
| 1953 | -** for check-in, wiki, or ticket. The remaining text is a | |
| 1976 | +** EVENTIDs are text. The first character is 'c', 'f', 't', or 'w' | |
| 1977 | +** for check-in, forum, ticket, or wiki. The remaining text is a | |
| 1954 | 1978 | ** integer that references the EVENT.OBJID value for the event. |
| 1955 | 1979 | ** Run /timeline?showid to see these OBJID values. |
| 1956 | 1980 | ** |
| 1957 | 1981 | ** If the --backoffice option is included, then email_backoffice() is run |
| 1958 | 1982 | ** after all alerts have been added. This will cause the alerts to |
| @@ -1984,11 +2008,35 @@ | ||
| 1984 | 2008 | #define SENDALERT_TRACE 0x0008 /* Trace operation for debugging */ |
| 1985 | 2009 | |
| 1986 | 2010 | #endif /* INTERFACE */ |
| 1987 | 2011 | |
| 1988 | 2012 | /* |
| 1989 | -** Send alert emails to all subscribers. | |
| 2013 | +** Send alert emails to subscribers. | |
| 2014 | +** | |
| 2015 | +** This procedure is run by either the backoffice, or in response to the | |
| 2016 | +** "fossil alerts send" command. Details of operation are controlled by | |
| 2017 | +** the flags parameter. | |
| 2018 | +** | |
| 2019 | +** Here is a summary of what happens: | |
| 2020 | +** | |
| 2021 | +** (1) Create a TEMP table wantalert(eventId,needMod) and fill it with | |
| 2022 | +** all the events that we want to send alerts about. The needMod | |
| 2023 | +** flags is set if and only if the event is still awaiting | |
| 2024 | +** moderator approval. Events with the needMod flag are only | |
| 2025 | +** shown to users that have moderator privileges. | |
| 2026 | +** | |
| 2027 | +** (2) Call email_compute_event_text() to compute a list of EmailEvent | |
| 2028 | +** objects that describe all events about which we want to send | |
| 2029 | +** alerts. | |
| 2030 | +** | |
| 2031 | +** (3) Loop over all subscribers. Compose and send one or more email | |
| 2032 | +** messages to each subscriber that describe the events for | |
| 2033 | +** which the subscriber has expressed interest and has | |
| 2034 | +** appropriate privileges. | |
| 2035 | +** | |
| 2036 | +** (4) Update the pending_alerts table to indicate that alerts have been | |
| 2037 | +** sent. | |
| 1990 | 2038 | */ |
| 1991 | 2039 | void email_send_alerts(u32 flags){ |
| 1992 | 2040 | EmailEvent *pEvents, *p; |
| 1993 | 2041 | int nEvent = 0; |
| 1994 | 2042 | Stmt q; |
| @@ -2001,10 +2049,11 @@ | ||
| 2001 | 2049 | EmailSender *pSender = 0; |
| 2002 | 2050 | u32 senderFlags = 0; |
| 2003 | 2051 | |
| 2004 | 2052 | if( g.fSqlTrace ) fossil_trace("-- BEGIN email_send_alerts(%u)\n", flags); |
| 2005 | 2053 | db_begin_transaction(); |
| 2054 | + email_schema(0); | |
| 2006 | 2055 | if( !email_enabled() ) goto send_alerts_done; |
| 2007 | 2056 | zUrl = db_get("email-url",0); |
| 2008 | 2057 | if( zUrl==0 ) goto send_alerts_done; |
| 2009 | 2058 | zRepoName = db_get("email-subname",0); |
| 2010 | 2059 | if( zRepoName==0 ) goto send_alerts_done; |
| @@ -2014,25 +2063,36 @@ | ||
| 2014 | 2063 | senderFlags |= EMAIL_TRACE; |
| 2015 | 2064 | } |
| 2016 | 2065 | pSender = email_sender_new(zDest, senderFlags); |
| 2017 | 2066 | db_multi_exec( |
| 2018 | 2067 | "DROP TABLE IF EXISTS temp.wantalert;" |
| 2019 | - "CREATE TEMP TABLE wantalert(eventId TEXT);" | |
| 2068 | + "CREATE TEMP TABLE wantalert(eventId TEXT, needMod BOOLEAN, sentMod);" | |
| 2020 | 2069 | ); |
| 2021 | 2070 | if( flags & SENDALERT_DIGEST ){ |
| 2071 | + /* Unmoderated changes are never sent as part of a digest */ | |
| 2022 | 2072 | db_multi_exec( |
| 2023 | - "INSERT INTO wantalert SELECT eventid FROM pending_alert" | |
| 2073 | + "INSERT INTO wantalert(eventId,needMod)" | |
| 2074 | + " SELECT eventid, 0" | |
| 2075 | + " FROM pending_alert" | |
| 2024 | 2076 | " WHERE sentDigest IS FALSE" |
| 2077 | + " AND NOT EXISTS(SELECT 1 FROM private WHERE rid=substr(eventid,2));" | |
| 2025 | 2078 | ); |
| 2026 | 2079 | zDigest = "true"; |
| 2027 | 2080 | }else{ |
| 2081 | + /* Immediate alerts might include events that are subject to | |
| 2082 | + ** moderator approval */ | |
| 2028 | 2083 | db_multi_exec( |
| 2029 | - "INSERT INTO wantalert SELECT eventid FROM pending_alert" | |
| 2030 | - " WHERE sentSep IS FALSE" | |
| 2084 | + "INSERT INTO wantalert(eventId,needMod,sentMod)" | |
| 2085 | + " SELECT eventid," | |
| 2086 | + " EXISTS(SELECT 1 FROM private WHERE rid=substr(eventid,2))," | |
| 2087 | + " sentMod" | |
| 2088 | + " FROM pending_alert" | |
| 2089 | + " WHERE sentSep IS FALSE;" | |
| 2090 | + "DELETE FROM wantalert WHERE needMod AND sentMod;" | |
| 2031 | 2091 | ); |
| 2032 | 2092 | } |
| 2033 | - pEvents = email_compute_event_text(&nEvent); | |
| 2093 | + pEvents = email_compute_event_text(&nEvent, (flags & SENDALERT_DIGEST)!=0); | |
| 2034 | 2094 | if( nEvent==0 ) goto send_alerts_done; |
| 2035 | 2095 | blob_init(&hdr, 0, 0); |
| 2036 | 2096 | blob_init(&body, 0, 0); |
| 2037 | 2097 | db_prepare(&q, |
| 2038 | 2098 | "SELECT" |
| @@ -2051,13 +2111,26 @@ | ||
| 2051 | 2111 | const char *zEmail = db_column_text(&q, 1); |
| 2052 | 2112 | const char *zCap = db_column_text(&q, 3); |
| 2053 | 2113 | int nHit = 0; |
| 2054 | 2114 | for(p=pEvents; p; p=p->pNext){ |
| 2055 | 2115 | 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 */ | |
| 2116 | + if( p->needMod ){ | |
| 2117 | + /* For events that require moderator approval, only send an alert | |
| 2118 | + ** if the recipient is a moderator for that type of event */ | |
| 2119 | + char xType = '*'; | |
| 2120 | + switch( p->type ){ | |
| 2121 | + case 'f': xType = '5'; break; | |
| 2122 | + case 't': xType = 'q'; break; | |
| 2123 | + case 'w': xType = 'l'; break; | |
| 2124 | + } | |
| 2125 | + if( strchr(zCap,xType)==0 ) continue; | |
| 2126 | + }else if( strchr(zCap,'s')!=0 || strchr(zCap,'a')!=0 ){ | |
| 2127 | + /* Setup and admin users can get any notification that does not | |
| 2128 | + ** require moderation */ | |
| 2058 | 2129 | }else{ |
| 2130 | + /* Other users only see the alert if they have sufficient | |
| 2131 | + ** privilege to view the event itself */ | |
| 2059 | 2132 | char xType = '*'; |
| 2060 | 2133 | switch( p->type ){ |
| 2061 | 2134 | case 'c': xType = 'o'; break; |
| 2062 | 2135 | case 'f': xType = '2'; break; |
| 2063 | 2136 | case 't': xType = 'r'; break; |
| @@ -2089,15 +2162,24 @@ | ||
| 2089 | 2162 | blob_reset(&body); |
| 2090 | 2163 | db_finalize(&q); |
| 2091 | 2164 | email_free_eventlist(pEvents); |
| 2092 | 2165 | if( (flags & SENDALERT_PRESERVE)==0 ){ |
| 2093 | 2166 | if( flags & SENDALERT_DIGEST ){ |
| 2094 | - db_multi_exec("UPDATE pending_alert SET sentDigest=true"); | |
| 2167 | + db_multi_exec( | |
| 2168 | + "UPDATE pending_alert SET sentDigest=true" | |
| 2169 | + " WHERE eventid IN (SELECT eventid FROM wantalert);" | |
| 2170 | + "DELETE FROM pending_alert WHERE sentDigest AND sentSep;" | |
| 2171 | + ); | |
| 2095 | 2172 | }else{ |
| 2096 | - db_multi_exec("UPDATE pending_alert SET sentSep=true"); | |
| 2173 | + db_multi_exec( | |
| 2174 | + "UPDATE pending_alert SET sentSep=true" | |
| 2175 | + " WHERE eventid IN (SELECT eventid FROM wantalert WHERE NOT needMod);" | |
| 2176 | + "UPDATE pending_alert SET sentMod=true" | |
| 2177 | + " WHERE eventid IN (SELECT eventid FROM wantalert WHERE needMod);" | |
| 2178 | + "DELETE FROM pending_alert WHERE sentDigest AND sentSep;" | |
| 2179 | + ); | |
| 2097 | 2180 | } |
| 2098 | - db_multi_exec("DELETE FROM pending_alert WHERE sentDigest AND sentSep"); | |
| 2099 | 2181 | } |
| 2100 | 2182 | send_alerts_done: |
| 2101 | 2183 | email_sender_free(pSender); |
| 2102 | 2184 | if( g.fSqlTrace ) fossil_trace("-- END email_send_alerts(%u)\n", flags); |
| 2103 | 2185 | db_end_transaction(0); |
| 2104 | 2186 |
| --- 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,80 +800,72 @@ | |
| 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 |
| 835 | ** a cron-job to make sure alerts are sent |
| 836 | ** in a timely manner. |
| 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 | ** |
| 850 | ** send TO [OPTIONS] Send a single email message using whatever |
| 851 | ** email sending mechanism is currently configured. |
| 852 | ** Use this for testing the email configuration. |
| 853 | ** Options: |
| 854 | ** |
| 855 | ** --body FILENAME |
| 856 | ** --smtp-trace |
| 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 | */ |
| 867 | void email_cmd(void){ |
| 868 | const char *zCmd; |
| 869 | int nCmd; |
| 870 | db_find_and_open_repository(0, 0); |
| 871 | email_schema(0); |
| 872 | zCmd = g.argc>=3 ? g.argv[2] : "x"; |
| 873 | nCmd = (int)strlen(zCmd); |
| 874 | if( strncmp(zCmd, "exec", nCmd)==0 ){ |
| 875 | u32 eFlags = 0; |
| 876 | if( find_option("digest",0,0)!=0 ) eFlags |= SENDALERT_DIGEST; |
| 877 | if( find_option("test",0,0)!=0 ){ |
| 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(); |
| @@ -923,10 +893,85 @@ | |
| 923 | ); |
| 924 | email_schema(0); |
| 925 | } |
| 926 | }else |
| 927 | if( strncmp(zCmd, "send", nCmd)==0 ){ |
| 928 | Blob prompt, body, hdr; |
| 929 | const char *zDest = find_option("stdout",0,0)!=0 ? "stdout" : 0; |
| 930 | int i; |
| 931 | u32 mFlags = EMAIL_IMMEDIATE_FAIL; |
| 932 | const char *zSubject = find_option("subject", "S", 1); |
| @@ -957,62 +1002,19 @@ | |
| 957 | email_sender_free(pSender); |
| 958 | blob_reset(&hdr); |
| 959 | blob_reset(&body); |
| 960 | blob_reset(&prompt); |
| 961 | }else |
| 962 | if( strncmp(zCmd, "settings", nCmd)==0 ){ |
| 963 | int isGlobal = find_option("global",0,0)!=0; |
| 964 | int nSetting; |
| 965 | const Setting *pSetting = setting_info(&nSetting); |
| 966 | db_open_config(1, 0); |
| 967 | verify_all_options(); |
| 968 | if( g.argc!=3 && g.argc!=5 ) usage("setting [NAME VALUE]"); |
| 969 | if( g.argc==5 ){ |
| 970 | const char *zLabel = g.argv[3]; |
| 971 | if( strncmp(zLabel, "email-", 6)!=0 |
| 972 | || (pSetting = db_find_setting(zLabel, 1))==0 ){ |
| 973 | fossil_fatal("not a valid email setting: \"%s\"", zLabel); |
| 974 | } |
| 975 | db_set(pSetting->name, g.argv[4], isGlobal); |
| 976 | g.argc = 3; |
| 977 | } |
| 978 | pSetting = setting_info(&nSetting); |
| 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 ){ |
| 989 | char *zPattern = g.argv[3]; |
| 990 | db_prepare(&q, |
| 991 | "SELECT semail FROM subscriber" |
| 992 | " WHERE semail LIKE '%%%q%%' OR suname LIKE '%%%q%%'" |
| 993 | " OR semail GLOB '*%q*' or suname GLOB '*%q*'" |
| 994 | " ORDER BY semail", |
| 995 | zPattern, zPattern, zPattern, zPattern); |
| 996 | }else{ |
| 997 | db_prepare(&q, |
| 998 | "SELECT semail FROM subscriber" |
| 999 | " ORDER BY semail"); |
| 1000 | } |
| 1001 | while( db_step(&q)==SQLITE_ROW ){ |
| 1002 | fossil_print("%s\n", db_column_text(&q, 0)); |
| 1003 | } |
| 1004 | db_finalize(&q); |
| 1005 | }else |
| 1006 | if( strncmp(zCmd, "unsubscribe", nCmd)==0 ){ |
| 1007 | verify_all_options(); |
| 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); |
| @@ -1947,12 +1971,12 @@ | |
| 1947 | ** |
| 1948 | ** Add one or more events to the pending_alert queue. Use this |
| 1949 | ** command during testing to force email notifications for specific |
| 1950 | ** events. |
| 1951 | ** |
| 1952 | ** EVENTIDs are text. The first character is 'c', 'w', or 't' |
| 1953 | ** for check-in, wiki, or ticket. The remaining text is a |
| 1954 | ** integer that references the EVENT.OBJID value for the event. |
| 1955 | ** Run /timeline?showid to see these OBJID values. |
| 1956 | ** |
| 1957 | ** If the --backoffice option is included, then email_backoffice() is run |
| 1958 | ** after all alerts have been added. This will cause the alerts to |
| @@ -1984,11 +2008,35 @@ | |
| 1984 | #define SENDALERT_TRACE 0x0008 /* Trace operation for debugging */ |
| 1985 | |
| 1986 | #endif /* INTERFACE */ |
| 1987 | |
| 1988 | /* |
| 1989 | ** Send alert emails to all subscribers. |
| 1990 | */ |
| 1991 | void email_send_alerts(u32 flags){ |
| 1992 | EmailEvent *pEvents, *p; |
| 1993 | int nEvent = 0; |
| 1994 | Stmt q; |
| @@ -2001,10 +2049,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 +2063,36 @@ | |
| 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 +2111,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 +2162,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,80 +800,72 @@ | |
| 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 | ** pending Show all pending alerts. Useful for debugging. |
| 812 | ** |
| 813 | ** reset Hard reset of all email notification tables |
| 814 | ** in the repository. This erases all subscription |
| 815 | ** information. ** Use with extreme care ** |
| 816 | ** |
| 817 | ** send Compose and send pending email alerts. |
| 818 | ** Some installations may want to do this via |
| 819 | ** a cron-job to make sure alerts are sent |
| 820 | ** in a timely manner. |
| 821 | ** Options: |
| 822 | ** |
| 823 | ** --digest Send digests |
| 824 | ** --test Write to standard output |
| 825 | ** |
| 826 | ** settings [NAME VALUE] With no arguments, list all email settings. |
| 827 | ** Or change the value of a single email setting. |
| 828 | ** |
| 829 | ** status Report on the status of the email alert |
| 830 | ** subsystem |
| 831 | ** |
| 832 | ** subscribers [PATTERN] List all subscribers matching PATTERN. |
| 833 | ** |
| 834 | ** test-message TO [OPTS] Send a single email message using whatever |
| 835 | ** email sending mechanism is currently configured. |
| 836 | ** Use this for testing the email notification |
| 837 | ** configuration. Options: |
| 838 | ** |
| 839 | ** --body FILENAME |
| 840 | ** --smtp-trace |
| 841 | ** --stdout |
| 842 | ** --subject|-S SUBJECT |
| 843 | ** |
| 844 | ** unsubscribe EMAIL Remove a single subscriber with the given EMAIL. |
| 845 | */ |
| 846 | void email_cmd(void){ |
| 847 | const char *zCmd; |
| 848 | int nCmd; |
| 849 | db_find_and_open_repository(0, 0); |
| 850 | email_schema(0); |
| 851 | zCmd = g.argc>=3 ? g.argv[2] : "x"; |
| 852 | nCmd = (int)strlen(zCmd); |
| 853 | if( strncmp(zCmd, "pending", nCmd)==0 ){ |
| 854 | Stmt q; |
| 855 | verify_all_options(); |
| 856 | if( g.argc!=3 ) usage("pending"); |
| 857 | db_prepare(&q,"SELECT eventid, sentSep, sentDigest, sentMod" |
| 858 | " FROM pending_alert"); |
| 859 | while( db_step(&q)==SQLITE_ROW ){ |
| 860 | fossil_print("%10s %7s %10s %7s\n", |
| 861 | db_column_text(&q,0), |
| 862 | db_column_int(&q,1) ? "sentSep" : "", |
| 863 | db_column_int(&q,2) ? "sentDigest" : "", |
| 864 | db_column_int(&q,3) ? "sentMod" : ""); |
| 865 | } |
| 866 | db_finalize(&q); |
| 867 | }else |
| 868 | if( strncmp(zCmd, "reset", nCmd)==0 ){ |
| 869 | int c; |
| 870 | int bForce = find_option("force","f",0)!=0; |
| 871 | verify_all_options(); |
| @@ -923,10 +893,85 @@ | |
| 893 | ); |
| 894 | email_schema(0); |
| 895 | } |
| 896 | }else |
| 897 | if( strncmp(zCmd, "send", nCmd)==0 ){ |
| 898 | u32 eFlags = 0; |
| 899 | if( find_option("digest",0,0)!=0 ) eFlags |= SENDALERT_DIGEST; |
| 900 | if( find_option("test",0,0)!=0 ){ |
| 901 | eFlags |= SENDALERT_PRESERVE|SENDALERT_STDOUT; |
| 902 | } |
| 903 | verify_all_options(); |
| 904 | email_send_alerts(eFlags); |
| 905 | }else |
| 906 | if( strncmp(zCmd, "settings", nCmd)==0 ){ |
| 907 | int isGlobal = find_option("global",0,0)!=0; |
| 908 | int nSetting; |
| 909 | const Setting *pSetting = setting_info(&nSetting); |
| 910 | db_open_config(1, 0); |
| 911 | verify_all_options(); |
| 912 | if( g.argc!=3 && g.argc!=5 ) usage("setting [NAME VALUE]"); |
| 913 | if( g.argc==5 ){ |
| 914 | const char *zLabel = g.argv[3]; |
| 915 | if( strncmp(zLabel, "email-", 6)!=0 |
| 916 | || (pSetting = db_find_setting(zLabel, 1))==0 ){ |
| 917 | fossil_fatal("not a valid email setting: \"%s\"", zLabel); |
| 918 | } |
| 919 | db_set(pSetting->name, g.argv[4], isGlobal); |
| 920 | g.argc = 3; |
| 921 | } |
| 922 | pSetting = setting_info(&nSetting); |
| 923 | for(; nSetting>0; nSetting--, pSetting++ ){ |
| 924 | if( strncmp(pSetting->name,"email-",6)!=0 ) continue; |
| 925 | print_setting(pSetting); |
| 926 | } |
| 927 | }else |
| 928 | if( strncmp(zCmd, "status", nCmd)==0 ){ |
| 929 | int nSetting, n; |
| 930 | static const char *zFmt = "%-29s %d\n"; |
| 931 | const Setting *pSetting = setting_info(&nSetting); |
| 932 | db_open_config(1, 0); |
| 933 | verify_all_options(); |
| 934 | if( g.argc!=3 ) usage("status"); |
| 935 | pSetting = setting_info(&nSetting); |
| 936 | for(; nSetting>0; nSetting--, pSetting++ ){ |
| 937 | if( strncmp(pSetting->name,"email-",6)!=0 ) continue; |
| 938 | print_setting(pSetting); |
| 939 | } |
| 940 | n = db_int(0,"SELECT count(*) FROM pending_alert WHERE NOT sentSep"); |
| 941 | fossil_print(zFmt/*works-like:"%s%d"*/, "pending-alerts", n); |
| 942 | n = db_int(0,"SELECT count(*) FROM pending_alert WHERE NOT sentDigest"); |
| 943 | fossil_print(zFmt/*works-like:"%s%d"*/, "pending-digest-alerts", n); |
| 944 | n = db_int(0,"SELECT count(*) FROM subscriber"); |
| 945 | fossil_print(zFmt/*works-like:"%s%d"*/, "total-subscribers", n); |
| 946 | n = db_int(0, "SELECT count(*) FROM subscriber WHERE sverified" |
| 947 | " AND NOT sdonotcall AND length(ssub)>1"); |
| 948 | fossil_print(zFmt/*works-like:"%s%d"*/, "active-subscribers", n); |
| 949 | }else |
| 950 | if( strncmp(zCmd, "subscribers", nCmd)==0 ){ |
| 951 | Stmt q; |
| 952 | verify_all_options(); |
| 953 | if( g.argc!=3 && g.argc!=4 ) usage("subscribers [PATTERN]"); |
| 954 | if( g.argc==4 ){ |
| 955 | char *zPattern = g.argv[3]; |
| 956 | db_prepare(&q, |
| 957 | "SELECT semail FROM subscriber" |
| 958 | " WHERE semail LIKE '%%%q%%' OR suname LIKE '%%%q%%'" |
| 959 | " OR semail GLOB '*%q*' or suname GLOB '*%q*'" |
| 960 | " ORDER BY semail", |
| 961 | zPattern, zPattern, zPattern, zPattern); |
| 962 | }else{ |
| 963 | db_prepare(&q, |
| 964 | "SELECT semail FROM subscriber" |
| 965 | " ORDER BY semail"); |
| 966 | } |
| 967 | while( db_step(&q)==SQLITE_ROW ){ |
| 968 | fossil_print("%s\n", db_column_text(&q, 0)); |
| 969 | } |
| 970 | db_finalize(&q); |
| 971 | }else |
| 972 | if( strncmp(zCmd, "test-message", nCmd)==0 ){ |
| 973 | Blob prompt, body, hdr; |
| 974 | const char *zDest = find_option("stdout",0,0)!=0 ? "stdout" : 0; |
| 975 | int i; |
| 976 | u32 mFlags = EMAIL_IMMEDIATE_FAIL; |
| 977 | const char *zSubject = find_option("subject", "S", 1); |
| @@ -957,62 +1002,19 @@ | |
| 1002 | email_sender_free(pSender); |
| 1003 | blob_reset(&hdr); |
| 1004 | blob_reset(&body); |
| 1005 | blob_reset(&prompt); |
| 1006 | }else |
| 1007 | if( strncmp(zCmd, "unsubscribe", nCmd)==0 ){ |
| 1008 | verify_all_options(); |
| 1009 | if( g.argc!=4 ) usage("unsubscribe EMAIL"); |
| 1010 | db_multi_exec( |
| 1011 | "DELETE FROM subscriber WHERE semail=%Q", g.argv[3]); |
| 1012 | }else |
| 1013 | { |
| 1014 | usage("pending|reset|send|setting|status|" |
| 1015 | "subscribers|test-message|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); |
| @@ -1947,12 +1971,12 @@ | |
| 1971 | ** |
| 1972 | ** Add one or more events to the pending_alert queue. Use this |
| 1973 | ** command during testing to force email notifications for specific |
| 1974 | ** events. |
| 1975 | ** |
| 1976 | ** EVENTIDs are text. The first character is 'c', 'f', 't', or 'w' |
| 1977 | ** for check-in, forum, ticket, or wiki. The remaining text is a |
| 1978 | ** integer that references the EVENT.OBJID value for the event. |
| 1979 | ** Run /timeline?showid to see these OBJID values. |
| 1980 | ** |
| 1981 | ** If the --backoffice option is included, then email_backoffice() is run |
| 1982 | ** after all alerts have been added. This will cause the alerts to |
| @@ -1984,11 +2008,35 @@ | |
| 2008 | #define SENDALERT_TRACE 0x0008 /* Trace operation for debugging */ |
| 2009 | |
| 2010 | #endif /* INTERFACE */ |
| 2011 | |
| 2012 | /* |
| 2013 | ** Send alert emails to subscribers. |
| 2014 | ** |
| 2015 | ** This procedure is run by either the backoffice, or in response to the |
| 2016 | ** "fossil alerts send" command. Details of operation are controlled by |
| 2017 | ** the flags parameter. |
| 2018 | ** |
| 2019 | ** Here is a summary of what happens: |
| 2020 | ** |
| 2021 | ** (1) Create a TEMP table wantalert(eventId,needMod) and fill it with |
| 2022 | ** all the events that we want to send alerts about. The needMod |
| 2023 | ** flags is set if and only if the event is still awaiting |
| 2024 | ** moderator approval. Events with the needMod flag are only |
| 2025 | ** shown to users that have moderator privileges. |
| 2026 | ** |
| 2027 | ** (2) Call email_compute_event_text() to compute a list of EmailEvent |
| 2028 | ** objects that describe all events about which we want to send |
| 2029 | ** alerts. |
| 2030 | ** |
| 2031 | ** (3) Loop over all subscribers. Compose and send one or more email |
| 2032 | ** messages to each subscriber that describe the events for |
| 2033 | ** which the subscriber has expressed interest and has |
| 2034 | ** appropriate privileges. |
| 2035 | ** |
| 2036 | ** (4) Update the pending_alerts table to indicate that alerts have been |
| 2037 | ** sent. |
| 2038 | */ |
| 2039 | void email_send_alerts(u32 flags){ |
| 2040 | EmailEvent *pEvents, *p; |
| 2041 | int nEvent = 0; |
| 2042 | Stmt q; |
| @@ -2001,10 +2049,11 @@ | |
| 2049 | EmailSender *pSender = 0; |
| 2050 | u32 senderFlags = 0; |
| 2051 | |
| 2052 | if( g.fSqlTrace ) fossil_trace("-- BEGIN email_send_alerts(%u)\n", flags); |
| 2053 | db_begin_transaction(); |
| 2054 | email_schema(0); |
| 2055 | if( !email_enabled() ) goto send_alerts_done; |
| 2056 | zUrl = db_get("email-url",0); |
| 2057 | if( zUrl==0 ) goto send_alerts_done; |
| 2058 | zRepoName = db_get("email-subname",0); |
| 2059 | if( zRepoName==0 ) goto send_alerts_done; |
| @@ -2014,25 +2063,36 @@ | |
| 2063 | senderFlags |= EMAIL_TRACE; |
| 2064 | } |
| 2065 | pSender = email_sender_new(zDest, senderFlags); |
| 2066 | db_multi_exec( |
| 2067 | "DROP TABLE IF EXISTS temp.wantalert;" |
| 2068 | "CREATE TEMP TABLE wantalert(eventId TEXT, needMod BOOLEAN, sentMod);" |
| 2069 | ); |
| 2070 | if( flags & SENDALERT_DIGEST ){ |
| 2071 | /* Unmoderated changes are never sent as part of a digest */ |
| 2072 | db_multi_exec( |
| 2073 | "INSERT INTO wantalert(eventId,needMod)" |
| 2074 | " SELECT eventid, 0" |
| 2075 | " FROM pending_alert" |
| 2076 | " WHERE sentDigest IS FALSE" |
| 2077 | " AND NOT EXISTS(SELECT 1 FROM private WHERE rid=substr(eventid,2));" |
| 2078 | ); |
| 2079 | zDigest = "true"; |
| 2080 | }else{ |
| 2081 | /* Immediate alerts might include events that are subject to |
| 2082 | ** moderator approval */ |
| 2083 | db_multi_exec( |
| 2084 | "INSERT INTO wantalert(eventId,needMod,sentMod)" |
| 2085 | " SELECT eventid," |
| 2086 | " EXISTS(SELECT 1 FROM private WHERE rid=substr(eventid,2))," |
| 2087 | " sentMod" |
| 2088 | " FROM pending_alert" |
| 2089 | " WHERE sentSep IS FALSE;" |
| 2090 | "DELETE FROM wantalert WHERE needMod AND sentMod;" |
| 2091 | ); |
| 2092 | } |
| 2093 | pEvents = email_compute_event_text(&nEvent, (flags & SENDALERT_DIGEST)!=0); |
| 2094 | if( nEvent==0 ) goto send_alerts_done; |
| 2095 | blob_init(&hdr, 0, 0); |
| 2096 | blob_init(&body, 0, 0); |
| 2097 | db_prepare(&q, |
| 2098 | "SELECT" |
| @@ -2051,13 +2111,26 @@ | |
| 2111 | const char *zEmail = db_column_text(&q, 1); |
| 2112 | const char *zCap = db_column_text(&q, 3); |
| 2113 | int nHit = 0; |
| 2114 | for(p=pEvents; p; p=p->pNext){ |
| 2115 | if( strchr(zSub,p->type)==0 ) continue; |
| 2116 | if( p->needMod ){ |
| 2117 | /* For events that require moderator approval, only send an alert |
| 2118 | ** if the recipient is a moderator for that type of event */ |
| 2119 | char xType = '*'; |
| 2120 | switch( p->type ){ |
| 2121 | case 'f': xType = '5'; break; |
| 2122 | case 't': xType = 'q'; break; |
| 2123 | case 'w': xType = 'l'; break; |
| 2124 | } |
| 2125 | if( strchr(zCap,xType)==0 ) continue; |
| 2126 | }else if( strchr(zCap,'s')!=0 || strchr(zCap,'a')!=0 ){ |
| 2127 | /* Setup and admin users can get any notification that does not |
| 2128 | ** require moderation */ |
| 2129 | }else{ |
| 2130 | /* Other users only see the alert if they have sufficient |
| 2131 | ** privilege to view the event itself */ |
| 2132 | char xType = '*'; |
| 2133 | switch( p->type ){ |
| 2134 | case 'c': xType = 'o'; break; |
| 2135 | case 'f': xType = '2'; break; |
| 2136 | case 't': xType = 'r'; break; |
| @@ -2089,15 +2162,24 @@ | |
| 2162 | blob_reset(&body); |
| 2163 | db_finalize(&q); |
| 2164 | email_free_eventlist(pEvents); |
| 2165 | if( (flags & SENDALERT_PRESERVE)==0 ){ |
| 2166 | if( flags & SENDALERT_DIGEST ){ |
| 2167 | db_multi_exec( |
| 2168 | "UPDATE pending_alert SET sentDigest=true" |
| 2169 | " WHERE eventid IN (SELECT eventid FROM wantalert);" |
| 2170 | "DELETE FROM pending_alert WHERE sentDigest AND sentSep;" |
| 2171 | ); |
| 2172 | }else{ |
| 2173 | db_multi_exec( |
| 2174 | "UPDATE pending_alert SET sentSep=true" |
| 2175 | " WHERE eventid IN (SELECT eventid FROM wantalert WHERE NOT needMod);" |
| 2176 | "UPDATE pending_alert SET sentMod=true" |
| 2177 | " WHERE eventid IN (SELECT eventid FROM wantalert WHERE needMod);" |
| 2178 | "DELETE FROM pending_alert WHERE sentDigest AND sentSep;" |
| 2179 | ); |
| 2180 | } |
| 2181 | } |
| 2182 | send_alerts_done: |
| 2183 | email_sender_free(pSender); |
| 2184 | if( g.fSqlTrace ) fossil_trace("-- END email_send_alerts(%u)\n", flags); |
| 2185 | db_end_transaction(0); |
| 2186 |
+2
-1
| --- src/webmail.c | ||
| +++ src/webmail.c | ||
| @@ -708,11 +708,11 @@ | ||
| 708 | 708 | @ <tr><td align="left"> |
| 709 | 709 | if( d==2 ){ |
| 710 | 710 | @ <input type="submit" name="read" value="Undelete"> |
| 711 | 711 | @ <input type="submit" name="purge" value="Delete Permanently"> |
| 712 | 712 | }else{ |
| 713 | - @ <input type="submit" name="trash", value="Delete"> | |
| 713 | + @ <input type="submit" name="trash" value="Delete"> | |
| 714 | 714 | if( d!=1 ){ |
| 715 | 715 | @ <input type="submit" name="unread" value="Mark as unread"> |
| 716 | 716 | } |
| 717 | 717 | @ <input type="submit" name="read" value="Mark as read"> |
| 718 | 718 | } |
| @@ -732,10 +732,11 @@ | ||
| 732 | 732 | while( db_step(&q)==SQLITE_ROW ){ |
| 733 | 733 | const char *zId = db_column_text(&q,0); |
| 734 | 734 | const char *zFrom = db_column_text(&q, 1); |
| 735 | 735 | const char *zDate = db_column_text(&q, 2); |
| 736 | 736 | const char *zSubject = db_column_text(&q, 4); |
| 737 | + if( zSubject==0 || zSubject[0]==0 ) zSubject = "(no subject)"; | |
| 737 | 738 | @ <tr> |
| 738 | 739 | @ <td><input type="checkbox" class="webmailckbox" name="e%s(zId)"></td> |
| 739 | 740 | @ <td>%h(zFrom)</td> |
| 740 | 741 | @ <td><a href="%s(url_render(&url,"id",zId,0,0))">%h(zSubject)</a> \ |
| 741 | 742 | @ %s(zDate)</td> |
| 742 | 743 |
| --- src/webmail.c | |
| +++ src/webmail.c | |
| @@ -708,11 +708,11 @@ | |
| 708 | @ <tr><td align="left"> |
| 709 | if( d==2 ){ |
| 710 | @ <input type="submit" name="read" value="Undelete"> |
| 711 | @ <input type="submit" name="purge" value="Delete Permanently"> |
| 712 | }else{ |
| 713 | @ <input type="submit" name="trash", value="Delete"> |
| 714 | if( d!=1 ){ |
| 715 | @ <input type="submit" name="unread" value="Mark as unread"> |
| 716 | } |
| 717 | @ <input type="submit" name="read" value="Mark as read"> |
| 718 | } |
| @@ -732,10 +732,11 @@ | |
| 732 | while( db_step(&q)==SQLITE_ROW ){ |
| 733 | const char *zId = db_column_text(&q,0); |
| 734 | const char *zFrom = db_column_text(&q, 1); |
| 735 | const char *zDate = db_column_text(&q, 2); |
| 736 | const char *zSubject = db_column_text(&q, 4); |
| 737 | @ <tr> |
| 738 | @ <td><input type="checkbox" class="webmailckbox" name="e%s(zId)"></td> |
| 739 | @ <td>%h(zFrom)</td> |
| 740 | @ <td><a href="%s(url_render(&url,"id",zId,0,0))">%h(zSubject)</a> \ |
| 741 | @ %s(zDate)</td> |
| 742 |
| --- src/webmail.c | |
| +++ src/webmail.c | |
| @@ -708,11 +708,11 @@ | |
| 708 | @ <tr><td align="left"> |
| 709 | if( d==2 ){ |
| 710 | @ <input type="submit" name="read" value="Undelete"> |
| 711 | @ <input type="submit" name="purge" value="Delete Permanently"> |
| 712 | }else{ |
| 713 | @ <input type="submit" name="trash" value="Delete"> |
| 714 | if( d!=1 ){ |
| 715 | @ <input type="submit" name="unread" value="Mark as unread"> |
| 716 | } |
| 717 | @ <input type="submit" name="read" value="Mark as read"> |
| 718 | } |
| @@ -732,10 +732,11 @@ | |
| 732 | while( db_step(&q)==SQLITE_ROW ){ |
| 733 | const char *zId = db_column_text(&q,0); |
| 734 | const char *zFrom = db_column_text(&q, 1); |
| 735 | const char *zDate = db_column_text(&q, 2); |
| 736 | const char *zSubject = db_column_text(&q, 4); |
| 737 | if( zSubject==0 || zSubject[0]==0 ) zSubject = "(no subject)"; |
| 738 | @ <tr> |
| 739 | @ <td><input type="checkbox" class="webmailckbox" name="e%s(zId)"></td> |
| 740 | @ <td>%h(zFrom)</td> |
| 741 | @ <td><a href="%s(url_render(&url,"id",zId,0,0))">%h(zSubject)</a> \ |
| 742 | @ %s(zDate)</td> |
| 743 |