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.

drh 2018-08-06 20:47 UTC forum-v2 merge
Commit 3c532ec55bf24a4c175976436cee6b6a61931aa9684c0934e8f1e74cd2008703
2 files changed +225 -143 +2 -1
+225 -143
--- src/email.c
+++ src/email.c
@@ -77,12 +77,13 @@
7777
@ -- Remaining characters determine the specific event. For example,
7878
@ -- 'c4413' means check-in with rid=4413.
7979
@ --
8080
@ CREATE TABLE repository.pending_alert(
8181
@ 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
8485
@ ) WITHOUT ROWID;
8586
@
8687
@ DROP TABLE IF EXISTS repository.email_bounce;
8788
@ -- Record bounced emails. If too many bounces are received within
8889
@ -- some defined time range, then cancel the subscription. Older
@@ -115,10 +116,15 @@
115116
){
116117
return; /* Don't create table for disabled email */
117118
}
118119
db_multi_exec(zEmailInit/*works-like:""*/);
119120
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
+ );
120126
}
121127
}
122128
123129
/*
124130
** Enable triggers that automatically populate the pending_alert
@@ -294,18 +300,10 @@
294300
@ <p>This is the email for the human administrator for the system.
295301
@ Abuse and trouble reports are send here.
296302
@ (Property: "email-admin")</p>
297303
@ <hr>
298304
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>
307305
@ <p><input type="submit" name="submit" value="Apply Changes" /></p>
308306
@ </div></form>
309307
db_end_transaction(0);
310308
style_footer();
311309
}
@@ -757,24 +755,10 @@
757755
fossil_print("%s", blob_str(&all));
758756
}
759757
blob_reset(&all);
760758
}
761759
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
-
776760
/*
777761
** SETTING: email-send-method width=5 default=off
778762
** Determine the method used to send email. Allowed values are
779763
** "off", "relay", "pipe", "dir", "db", and "stdout". The "off" value
780764
** means no email is ever sent. The "relay" value means emails are sent
@@ -806,16 +790,10 @@
806790
/*
807791
** SETTING: email-self width=40
808792
** This is the email address for the repository. Outbound emails add
809793
** this email address as the "From:" field.
810794
*/
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
-*/
817795
/*
818796
** SETTING: email-send-relayhost width=40
819797
** This is the hostname and TCP port to which output email messages
820798
** are sent when email-send-method is "relay". There should be an
821799
** SMTP server configured as a Mail Submission Agent listening on the
@@ -822,80 +800,72 @@
822800
** designated host and port and all times.
823801
*/
824802
825803
826804
/*
827
-** COMMAND: email
805
+** COMMAND: alerts
828806
**
829
-** Usage: %fossil email SUBCOMMAND ARGS...
807
+** Usage: %fossil alerts SUBCOMMAND ARGS...
830808
**
831809
** Subcommands:
832810
**
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.
834818
** Some installations may want to do this via
835819
** a cron-job to make sure alerts are sent
836820
** in a timely manner.
837821
** Options:
838822
**
839823
** --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
851835
** 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:
854838
**
855839
** --body FILENAME
856840
** --smtp-trace
857841
** --stdout
858842
** --subject|-S SUBJECT
859843
**
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
-**
865844
** unsubscribe EMAIL Remove a single subscriber with the given EMAIL.
866845
*/
867846
void email_cmd(void){
868847
const char *zCmd;
869848
int nCmd;
870849
db_find_and_open_repository(0, 0);
871850
email_schema(0);
872851
zCmd = g.argc>=3 ? g.argv[2] : "x";
873852
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);
897867
}else
898868
if( strncmp(zCmd, "reset", nCmd)==0 ){
899869
int c;
900870
int bForce = find_option("force","f",0)!=0;
901871
verify_all_options();
@@ -923,10 +893,85 @@
923893
);
924894
email_schema(0);
925895
}
926896
}else
927897
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 ){
928973
Blob prompt, body, hdr;
929974
const char *zDest = find_option("stdout",0,0)!=0 ? "stdout" : 0;
930975
int i;
931976
u32 mFlags = EMAIL_IMMEDIATE_FAIL;
932977
const char *zSubject = find_option("subject", "S", 1);
@@ -957,62 +1002,19 @@
9571002
email_sender_free(pSender);
9581003
blob_reset(&hdr);
9591004
blob_reset(&body);
9601005
blob_reset(&prompt);
9611006
}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
10061007
if( strncmp(zCmd, "unsubscribe", nCmd)==0 ){
10071008
verify_all_options();
10081009
if( g.argc!=4 ) usage("unsubscribe EMAIL");
10091010
db_multi_exec(
10101011
"DELETE FROM subscriber WHERE semail=%Q", g.argv[3]);
10111012
}else
10121013
{
1013
- usage("exec|inbound|reset|send|setting|subscribers|unsubscribe");
1014
+ usage("pending|reset|send|setting|status|"
1015
+ "subscribers|test-message|unsubscribe");
10141016
}
10151017
}
10161018
10171019
/*
10181020
** Do error checking on a submitted subscription form. Return TRUE
@@ -1789,11 +1791,12 @@
17891791
/*
17901792
** A single event that might appear in an alert is recorded as an
17911793
** instance of the following object.
17921794
*/
17931795
struct EmailEvent {
1794
- int type; /* 'c', 't', 'w', 'f' */
1796
+ int type; /* 'c', 'f', 'm', 't', 'w' */
1797
+ int needMod; /* Pending moderator approval */
17951798
Blob txt; /* Text description to appear in an alert */
17961799
EmailEvent *pNext; /* Next in chronological order */
17971800
};
17981801
#endif
17991802
@@ -1812,33 +1815,34 @@
18121815
/*
18131816
** Compute and return a linked list of EmailEvent objects
18141817
** corresponding to the current content of the temp.wantalert
18151818
** table which should be defined as follows:
18161819
**
1817
-** CREATE TEMP TABLE wantalert(eventId TEXT);
1820
+** CREATE TEMP TABLE wantalert(eventId TEXT, needMod BOOLEAN);
18181821
*/
1819
-EmailEvent *email_compute_event_text(int *pnEvent){
1822
+EmailEvent *email_compute_event_text(int *pnEvent, int doDigest){
18201823
Stmt q;
18211824
EmailEvent *p;
18221825
EmailEvent anchor;
18231826
EmailEvent *pLast;
18241827
const char *zUrl = db_get("email-url","http://localhost:8080");
18251828
18261829
db_prepare(&q,
18271830
"SELECT"
1828
- " blob.uuid," /* 0 */
1829
- " datetime(event.mtime)," /* 1 */
1831
+ " blob.uuid," /* 0 */
1832
+ " datetime(event.mtime)," /* 1 */
18301833
" coalesce(ecomment,comment)"
18311834
" || ' (user: ' || coalesce(euser,user,'?')"
18321835
" || (SELECT case when length(x)>0 then ' tags: ' || x else '' end"
18331836
" FROM (SELECT group_concat(substr(tagname,5), ', ') AS x"
18341837
" FROM tag, tagxref"
18351838
" WHERE tagname GLOB 'sym-*' AND tag.tagid=tagxref.tagid"
18361839
" AND tagxref.rid=blob.rid AND tagxref.tagtype>0))"
1837
- " || ')' as comment," /* 2 */
1840
+ " || ')' as comment," /* 2 */
18381841
" tagxref.value AS branch," /* 3 */
1839
- " wantalert.eventId" /* 4 */
1842
+ " wantalert.eventId," /* 4 */
1843
+ " wantalert.needMod" /* 5 */
18401844
" FROM temp.wantalert JOIN tag CROSS JOIN event CROSS JOIN blob"
18411845
" LEFT JOIN tagxref ON tagxref.tagid=tag.tagid"
18421846
" AND tagxref.tagtype>0"
18431847
" AND tagxref.rid=blob.rid"
18441848
" WHERE blob.rid=event.objid"
@@ -1853,13 +1857,15 @@
18531857
const char *zType = "";
18541858
p = fossil_malloc( sizeof(EmailEvent) );
18551859
pLast->pNext = p;
18561860
pLast = p;
18571861
p->type = db_column_text(&q, 4)[0];
1862
+ p->needMod = db_column_int(&q, 5);
18581863
p->pNext = 0;
18591864
switch( p->type ){
18601865
case 'c': zType = "Check-In"; break;
1866
+ case 'f': zType = "Forum post"; break;
18611867
case 't': zType = "Wiki Edit"; break;
18621868
case 'w': zType = "Ticket Change"; break;
18631869
}
18641870
blob_init(&p->txt, 0, 0);
18651871
blob_appendf(&p->txt,"== %s %s ==\n%s\n%s/info/%.20s\n",
@@ -1867,10 +1873,16 @@
18671873
zType,
18681874
db_column_text(&q,2),
18691875
zUrl,
18701876
db_column_text(&q,0)
18711877
);
1878
+ if( p->needMod ){
1879
+ blob_appendf(&p->txt,
1880
+ "** Pending moderator approval (%s/modreq) **\n",
1881
+ zUrl
1882
+ );
1883
+ }
18721884
(*pnEvent)++;
18731885
}
18741886
db_finalize(&q);
18751887
return anchor.pNext;
18761888
}
@@ -1905,32 +1917,44 @@
19051917
** command line, generate text for all events named in the
19061918
** pending_alert table.
19071919
**
19081920
** This command is intended for testing and debugging the logic
19091921
** that generates email alert text.
1922
+**
1923
+** Options:
1924
+**
1925
+** --digest Generate digest alert text
1926
+** --needmod Assume all events are pending moderator approval
19101927
*/
19111928
void test_alert_cmd(void){
19121929
Blob out;
19131930
int nEvent;
1931
+ int needMod;
1932
+ int doDigest;
19141933
EmailEvent *pEvent, *p;
19151934
1935
+ doDigest = find_option("digest",0,0)!=0;
1936
+ needMod = find_option("needmod",0,0)!=0;
19161937
db_find_and_open_repository(0, 0);
19171938
verify_all_options();
19181939
db_begin_transaction();
19191940
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)");
19211942
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);
19231946
}else{
19241947
int i;
19251948
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);
19271951
}
19281952
}
19291953
blob_init(&out, 0, 0);
19301954
email_header(&out);
1931
- pEvent = email_compute_event_text(&nEvent);
1955
+ pEvent = email_compute_event_text(&nEvent, doDigest);
19321956
for(p=pEvent; p; p=p->pNext){
19331957
blob_append(&out, "\n", 1);
19341958
blob_append(&out, blob_buffer(&p->txt), blob_size(&p->txt));
19351959
}
19361960
email_free_eventlist(pEvent);
@@ -1947,12 +1971,12 @@
19471971
**
19481972
** Add one or more events to the pending_alert queue. Use this
19491973
** command during testing to force email notifications for specific
19501974
** events.
19511975
**
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
19541978
** integer that references the EVENT.OBJID value for the event.
19551979
** Run /timeline?showid to see these OBJID values.
19561980
**
19571981
** If the --backoffice option is included, then email_backoffice() is run
19581982
** after all alerts have been added. This will cause the alerts to
@@ -1984,11 +2008,35 @@
19842008
#define SENDALERT_TRACE 0x0008 /* Trace operation for debugging */
19852009
19862010
#endif /* INTERFACE */
19872011
19882012
/*
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.
19902038
*/
19912039
void email_send_alerts(u32 flags){
19922040
EmailEvent *pEvents, *p;
19932041
int nEvent = 0;
19942042
Stmt q;
@@ -2001,10 +2049,11 @@
20012049
EmailSender *pSender = 0;
20022050
u32 senderFlags = 0;
20032051
20042052
if( g.fSqlTrace ) fossil_trace("-- BEGIN email_send_alerts(%u)\n", flags);
20052053
db_begin_transaction();
2054
+ email_schema(0);
20062055
if( !email_enabled() ) goto send_alerts_done;
20072056
zUrl = db_get("email-url",0);
20082057
if( zUrl==0 ) goto send_alerts_done;
20092058
zRepoName = db_get("email-subname",0);
20102059
if( zRepoName==0 ) goto send_alerts_done;
@@ -2014,25 +2063,36 @@
20142063
senderFlags |= EMAIL_TRACE;
20152064
}
20162065
pSender = email_sender_new(zDest, senderFlags);
20172066
db_multi_exec(
20182067
"DROP TABLE IF EXISTS temp.wantalert;"
2019
- "CREATE TEMP TABLE wantalert(eventId TEXT);"
2068
+ "CREATE TEMP TABLE wantalert(eventId TEXT, needMod BOOLEAN, sentMod);"
20202069
);
20212070
if( flags & SENDALERT_DIGEST ){
2071
+ /* Unmoderated changes are never sent as part of a digest */
20222072
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"
20242076
" WHERE sentDigest IS FALSE"
2077
+ " AND NOT EXISTS(SELECT 1 FROM private WHERE rid=substr(eventid,2));"
20252078
);
20262079
zDigest = "true";
20272080
}else{
2081
+ /* Immediate alerts might include events that are subject to
2082
+ ** moderator approval */
20282083
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;"
20312091
);
20322092
}
2033
- pEvents = email_compute_event_text(&nEvent);
2093
+ pEvents = email_compute_event_text(&nEvent, (flags & SENDALERT_DIGEST)!=0);
20342094
if( nEvent==0 ) goto send_alerts_done;
20352095
blob_init(&hdr, 0, 0);
20362096
blob_init(&body, 0, 0);
20372097
db_prepare(&q,
20382098
"SELECT"
@@ -2051,13 +2111,26 @@
20512111
const char *zEmail = db_column_text(&q, 1);
20522112
const char *zCap = db_column_text(&q, 3);
20532113
int nHit = 0;
20542114
for(p=pEvents; p; p=p->pNext){
20552115
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 */
20582129
}else{
2130
+ /* Other users only see the alert if they have sufficient
2131
+ ** privilege to view the event itself */
20592132
char xType = '*';
20602133
switch( p->type ){
20612134
case 'c': xType = 'o'; break;
20622135
case 'f': xType = '2'; break;
20632136
case 't': xType = 'r'; break;
@@ -2089,15 +2162,24 @@
20892162
blob_reset(&body);
20902163
db_finalize(&q);
20912164
email_free_eventlist(pEvents);
20922165
if( (flags & SENDALERT_PRESERVE)==0 ){
20932166
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
+ );
20952172
}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
+ );
20972180
}
2098
- db_multi_exec("DELETE FROM pending_alert WHERE sentDigest AND sentSep");
20992181
}
21002182
send_alerts_done:
21012183
email_sender_free(pSender);
21022184
if( g.fSqlTrace ) fossil_trace("-- END email_send_alerts(%u)\n", flags);
21032185
db_end_transaction(0);
21042186
--- 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 @@
708708
@ <tr><td align="left">
709709
if( d==2 ){
710710
@ <input type="submit" name="read" value="Undelete">
711711
@ <input type="submit" name="purge" value="Delete Permanently">
712712
}else{
713
- @ <input type="submit" name="trash", value="Delete">
713
+ @ <input type="submit" name="trash" value="Delete">
714714
if( d!=1 ){
715715
@ <input type="submit" name="unread" value="Mark as unread">
716716
}
717717
@ <input type="submit" name="read" value="Mark as read">
718718
}
@@ -732,10 +732,11 @@
732732
while( db_step(&q)==SQLITE_ROW ){
733733
const char *zId = db_column_text(&q,0);
734734
const char *zFrom = db_column_text(&q, 1);
735735
const char *zDate = db_column_text(&q, 2);
736736
const char *zSubject = db_column_text(&q, 4);
737
+ if( zSubject==0 || zSubject[0]==0 ) zSubject = "(no subject)";
737738
@ <tr>
738739
@ <td><input type="checkbox" class="webmailckbox" name="e%s(zId)"></td>
739740
@ <td>%h(zFrom)</td>
740741
@ <td><a href="%s(url_render(&url,"id",zId,0,0))">%h(zSubject)</a> \
741742
@ %s(zDate)</td>
742743
--- 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

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button