|
1
|
/* |
|
2
|
** Copyright (c) 2018 D. Richard Hipp |
|
3
|
** |
|
4
|
** This program is free software; you can redistribute it and/or |
|
5
|
** modify it under the terms of the Simplified BSD License (also |
|
6
|
** known as the "2-Clause License" or "FreeBSD License".) |
|
7
|
** |
|
8
|
** This program is distributed in the hope that it will be useful, |
|
9
|
** but without any warranty; without even the implied warranty of |
|
10
|
** merchantability or fitness for a particular purpose. |
|
11
|
** |
|
12
|
** Author contact information: |
|
13
|
** [email protected] |
|
14
|
** http://www.hwaci.com/drh/ |
|
15
|
** |
|
16
|
******************************************************************************* |
|
17
|
** |
|
18
|
** Logic for email notification, also known as "alerts" or "subscriptions". |
|
19
|
** |
|
20
|
** Are you looking for the code that reads and writes the internet |
|
21
|
** email protocol? That is not here. See the "smtp.c" file instead. |
|
22
|
** Yes, the choice of source code filenames is not the greatest, but |
|
23
|
** it is not so bad that changing them seems justified. |
|
24
|
*/ |
|
25
|
#include "config.h" |
|
26
|
#include "alerts.h" |
|
27
|
#include <assert.h> |
|
28
|
#include <time.h> |
|
29
|
|
|
30
|
/* |
|
31
|
** Maximum size of the subscriberCode blob, in bytes |
|
32
|
*/ |
|
33
|
#define SUBSCRIBER_CODE_SZ 32 |
|
34
|
|
|
35
|
/* |
|
36
|
** SQL code to implement the tables needed by the email notification |
|
37
|
** system. |
|
38
|
*/ |
|
39
|
static const char zAlertInit[] = |
|
40
|
@ DROP TABLE IF EXISTS repository.subscriber; |
|
41
|
@ -- Subscribers are distinct from users. A person can have a log-in in |
|
42
|
@ -- the USER table without being a subscriber. Or a person can be a |
|
43
|
@ -- subscriber without having a USER table entry. Or they can have both. |
|
44
|
@ -- In the last case the suname column points from the subscriber entry |
|
45
|
@ -- to the USER entry. |
|
46
|
@ -- |
|
47
|
@ -- The ssub field is a string where each character indicates a particular |
|
48
|
@ -- type of event to subscribe to. Choices: |
|
49
|
@ -- a - Announcements |
|
50
|
@ -- c - Check-ins |
|
51
|
@ -- f - Forum posts |
|
52
|
@ -- k - ** Special: Unsubscribed using /oneclickunsub |
|
53
|
@ -- n - New forum threads |
|
54
|
@ -- r - Replies to my own forum posts |
|
55
|
@ -- t - Ticket changes |
|
56
|
@ -- u - Changes of users' permissions (admins only) |
|
57
|
@ -- w - Wiki changes |
|
58
|
@ -- x - Edits to forum posts |
|
59
|
@ -- Probably different codes will be added in the future. In the future |
|
60
|
@ -- we might also add a separate table that allows subscribing to email |
|
61
|
@ -- notifications for specific branches or tags or tickets. |
|
62
|
@ -- |
|
63
|
@ CREATE TABLE repository.subscriber( |
|
64
|
@ subscriberId INTEGER PRIMARY KEY, -- numeric subscriber ID. Internal use |
|
65
|
@ subscriberCode BLOB DEFAULT (randomblob(32)) UNIQUE, -- UUID for subscriber |
|
66
|
@ semail TEXT UNIQUE COLLATE nocase,-- email address |
|
67
|
@ suname TEXT, -- corresponding USER entry |
|
68
|
@ sverified BOOLEAN DEFAULT true, -- email address verified |
|
69
|
@ sdonotcall BOOLEAN, -- true for Do Not Call |
|
70
|
@ sdigest BOOLEAN, -- true for daily digests only |
|
71
|
@ ssub TEXT, -- baseline subscriptions |
|
72
|
@ sctime INTDATE, -- When this entry was created. unixtime |
|
73
|
@ mtime INTDATE, -- Last change. unixtime |
|
74
|
@ smip TEXT, -- IP address of last change |
|
75
|
@ lastContact INT -- Last contact. days since 1970 |
|
76
|
@ ); |
|
77
|
@ CREATE INDEX repository.subscriberUname |
|
78
|
@ ON subscriber(suname) WHERE suname IS NOT NULL; |
|
79
|
@ |
|
80
|
@ DROP TABLE IF EXISTS repository.pending_alert; |
|
81
|
@ -- Email notifications that need to be sent. |
|
82
|
@ -- |
|
83
|
@ -- The first character of the eventid determines the event type. |
|
84
|
@ -- Remaining characters determine the specific event. For example, |
|
85
|
@ -- 'c4413' means check-in with rid=4413. |
|
86
|
@ -- |
|
87
|
@ CREATE TABLE repository.pending_alert( |
|
88
|
@ eventid TEXT PRIMARY KEY, -- Object that changed |
|
89
|
@ sentSep BOOLEAN DEFAULT false, -- individual alert sent |
|
90
|
@ sentDigest BOOLEAN DEFAULT false, -- digest alert sent |
|
91
|
@ sentMod BOOLEAN DEFAULT false -- pending moderation alert sent |
|
92
|
@ ) WITHOUT ROWID; |
|
93
|
@ |
|
94
|
@ -- Obsolete table. No longer used. |
|
95
|
@ DROP TABLE IF EXISTS repository.alert_bounce; |
|
96
|
; |
|
97
|
|
|
98
|
/* |
|
99
|
** Return true if the email notification tables exist. |
|
100
|
*/ |
|
101
|
int alert_tables_exist(void){ |
|
102
|
return db_table_exists("repository", "subscriber"); |
|
103
|
} |
|
104
|
|
|
105
|
/* |
|
106
|
** Record the fact that user zUser has made contact with the repository. |
|
107
|
** This resets the subscription timeout on that user. |
|
108
|
*/ |
|
109
|
void alert_user_contact(const char *zUser){ |
|
110
|
if( db_table_has_column("repository","subscriber","lastContact") ){ |
|
111
|
db_unprotect(PROTECT_READONLY); |
|
112
|
db_multi_exec( |
|
113
|
"UPDATE subscriber SET lastContact=now()/86400 WHERE suname=%Q", |
|
114
|
zUser |
|
115
|
); |
|
116
|
db_protect_pop(); |
|
117
|
} |
|
118
|
} |
|
119
|
|
|
120
|
/* |
|
121
|
** Make sure the table needed for email notification exist in the repository. |
|
122
|
** |
|
123
|
** If the bOnlyIfEnabled option is true, then tables are only created |
|
124
|
** if the email-send-method is something other than "off". |
|
125
|
*/ |
|
126
|
void alert_schema(int bOnlyIfEnabled){ |
|
127
|
if( !alert_tables_exist() ){ |
|
128
|
if( bOnlyIfEnabled |
|
129
|
&& fossil_strcmp(db_get("email-send-method",0),"off")==0 |
|
130
|
){ |
|
131
|
return; /* Don't create table for disabled email */ |
|
132
|
} |
|
133
|
db_exec_sql(zAlertInit); |
|
134
|
return; |
|
135
|
} |
|
136
|
if( db_table_has_column("repository","subscriber","lastContact") ){ |
|
137
|
return; |
|
138
|
} |
|
139
|
db_unprotect(PROTECT_READONLY); |
|
140
|
db_multi_exec( |
|
141
|
"DROP TABLE IF EXISTS repository.alert_bounce;\n" |
|
142
|
"ALTER TABLE repository.subscriber ADD COLUMN lastContact INT;\n" |
|
143
|
"UPDATE subscriber SET lastContact=mtime/86400;" |
|
144
|
); |
|
145
|
db_protect_pop(); |
|
146
|
if( db_table_has_column("repository","pending_alert","sentMod") ){ |
|
147
|
return; |
|
148
|
} |
|
149
|
db_multi_exec( |
|
150
|
"ALTER TABLE repository.pending_alert" |
|
151
|
" ADD COLUMN sentMod BOOLEAN DEFAULT false;" |
|
152
|
); |
|
153
|
} |
|
154
|
|
|
155
|
/* |
|
156
|
** Process deferred alert events. Return the number of errors. |
|
157
|
*/ |
|
158
|
static int alert_process_deferred_triggers(void){ |
|
159
|
if( db_table_exists("temp","deferred_chat_events") |
|
160
|
&& db_table_exists("repository","chat") |
|
161
|
){ |
|
162
|
const char *zChatUser = db_get("chat-timeline-user", 0); |
|
163
|
if( zChatUser && zChatUser[0] ){ |
|
164
|
chat_create_tables(); /* Make sure TEMP TRIGGERs for FTS exist */ |
|
165
|
db_multi_exec( |
|
166
|
"INSERT INTO chat(mtime,lmtime,xfrom,xmsg)" |
|
167
|
" SELECT julianday(), " |
|
168
|
" strftime('%%Y-%%m-%%dT%%H:%%M:%%S','now','localtime')," |
|
169
|
" %Q," |
|
170
|
" chat_msg_from_event(type, objid, user, comment)\n" |
|
171
|
" FROM deferred_chat_events;\n", |
|
172
|
zChatUser |
|
173
|
); |
|
174
|
} |
|
175
|
} |
|
176
|
return 0; |
|
177
|
} |
|
178
|
|
|
179
|
/* |
|
180
|
** Enable triggers that automatically populate the pending_alert |
|
181
|
** table. (Later:) Also add triggers that automatically relay timeline |
|
182
|
** events to chat, if chat is configured for that. |
|
183
|
*/ |
|
184
|
void alert_create_trigger(void){ |
|
185
|
if( db_table_exists("repository","pending_alert") ){ |
|
186
|
db_multi_exec( |
|
187
|
"DROP TRIGGER IF EXISTS repository.alert_trigger1;\n" /* Purge legacy */ |
|
188
|
"CREATE TRIGGER temp.alert_trigger1\n" |
|
189
|
"AFTER INSERT ON repository.event BEGIN\n" |
|
190
|
" INSERT INTO pending_alert(eventid)\n" |
|
191
|
" SELECT printf('%%.1c%%d',new.type,new.objid) WHERE true\n" |
|
192
|
" ON CONFLICT(eventId) DO NOTHING;\n" |
|
193
|
"END;" |
|
194
|
); |
|
195
|
} |
|
196
|
if( db_table_exists("repository","chat") |
|
197
|
&& db_get("chat-timeline-user", "")[0]!=0 |
|
198
|
){ |
|
199
|
/* Record events that will be relayed to chat, but do not relay |
|
200
|
** them immediately, as the chat_msg_from_event() function requires |
|
201
|
** that TAGXREF be up-to-date, and that has not happened yet when |
|
202
|
** the insert into the EVENT table occurs. Make arrangements to |
|
203
|
** invoke alert_process_deferred_triggers() when the transaction |
|
204
|
** commits. The TAGXREF table will be ready by then. */ |
|
205
|
db_multi_exec( |
|
206
|
"CREATE TABLE temp.deferred_chat_events(\n" |
|
207
|
" type TEXT,\n" |
|
208
|
" objid INT,\n" |
|
209
|
" user TEXT,\n" |
|
210
|
" comment TEXT\n" |
|
211
|
");\n" |
|
212
|
"CREATE TRIGGER temp.chat_trigger1\n" |
|
213
|
"AFTER INSERT ON repository.event BEGIN\n" |
|
214
|
" INSERT INTO deferred_chat_events" |
|
215
|
" VALUES(new.type,new.objid,new.user,new.comment);\n" |
|
216
|
"END;\n" |
|
217
|
); |
|
218
|
db_commit_hook(alert_process_deferred_triggers, 1); |
|
219
|
} |
|
220
|
} |
|
221
|
|
|
222
|
/* |
|
223
|
** Disable triggers the event_pending and chat triggers. |
|
224
|
** |
|
225
|
** This must be called before rebuilding the EVENT table, for example |
|
226
|
** via the "fossil rebuild" command. |
|
227
|
*/ |
|
228
|
void alert_drop_trigger(void){ |
|
229
|
db_multi_exec( |
|
230
|
"DROP TRIGGER IF EXISTS temp.alert_trigger1;\n" |
|
231
|
"DROP TRIGGER IF EXISTS repository.alert_trigger1;\n" /* Purge legacy */ |
|
232
|
"DROP TRIGGER IF EXISTS temp.chat_trigger1;\n" |
|
233
|
); |
|
234
|
} |
|
235
|
|
|
236
|
/* |
|
237
|
** Return true if email alerts are active. |
|
238
|
*/ |
|
239
|
int alert_enabled(void){ |
|
240
|
if( !alert_tables_exist() ) return 0; |
|
241
|
if( fossil_strcmp(db_get("email-send-method",0),"off")==0 ) return 0; |
|
242
|
return 1; |
|
243
|
} |
|
244
|
|
|
245
|
/* |
|
246
|
** If alerts are enabled, removes the pending_alert entry which |
|
247
|
** matches (eventType || rid). Note that pending_alert entries are |
|
248
|
** added via the manifest crosslinking process, so this has no effect |
|
249
|
** if called before crosslinking is performed. Because alerts are sent |
|
250
|
** asynchronously, unqueuing needs to be performed as part of the |
|
251
|
** transaction in which crosslinking is performed in order to avoid a |
|
252
|
** race condition. |
|
253
|
*/ |
|
254
|
void alert_unqueue(char eventType, int rid){ |
|
255
|
if( alert_enabled() ){ |
|
256
|
db_multi_exec("DELETE FROM pending_alert WHERE eventid='%c%d'", |
|
257
|
eventType, rid); |
|
258
|
} |
|
259
|
} |
|
260
|
|
|
261
|
/* |
|
262
|
** If the subscriber table does not exist, then paint an error message |
|
263
|
** web page and return true. |
|
264
|
** |
|
265
|
** If the subscriber table does exist, return 0 without doing anything. |
|
266
|
*/ |
|
267
|
static int alert_webpages_disabled(void){ |
|
268
|
if( alert_tables_exist() ) return 0; |
|
269
|
style_set_current_feature("alerts"); |
|
270
|
style_header("Email Alerts Are Disabled"); |
|
271
|
@ <p>Email alerts are disabled on this server</p> |
|
272
|
style_finish_page(); |
|
273
|
return 1; |
|
274
|
} |
|
275
|
|
|
276
|
/* |
|
277
|
** Insert a "Subscriber List" submenu link if the current user |
|
278
|
** is an administrator. |
|
279
|
*/ |
|
280
|
void alert_submenu_common(void){ |
|
281
|
if( g.perm.Admin ){ |
|
282
|
if( fossil_strcmp(g.zPath,"subscribers") ){ |
|
283
|
style_submenu_element("Subscribers","%R/subscribers"); |
|
284
|
} |
|
285
|
if( fossil_strcmp(g.zPath,"subscribe") ){ |
|
286
|
style_submenu_element("Add New Subscriber","%R/subscribe"); |
|
287
|
} |
|
288
|
if( fossil_strcmp(g.zPath,"setup_notification") ){ |
|
289
|
style_submenu_element("Notification Setup","%R/setup_notification"); |
|
290
|
} |
|
291
|
} |
|
292
|
} |
|
293
|
|
|
294
|
|
|
295
|
/* |
|
296
|
** WEBPAGE: setup_notification |
|
297
|
** |
|
298
|
** Administrative page for configuring and controlling email notification. |
|
299
|
** Normally accessible via the /Admin/Notification menu. |
|
300
|
*/ |
|
301
|
void setup_notification(void){ |
|
302
|
static const char *const azSendMethods[] = { |
|
303
|
"off", "Disabled", |
|
304
|
"relay", "SMTP relay", |
|
305
|
"db", "Store in a database", |
|
306
|
"dir", "Store in a directory", |
|
307
|
"pipe", "Pipe to a command", |
|
308
|
}; |
|
309
|
login_check_credentials(); |
|
310
|
if( !g.perm.Setup ){ |
|
311
|
login_needed(0); |
|
312
|
return; |
|
313
|
} |
|
314
|
db_begin_transaction(); |
|
315
|
|
|
316
|
alert_submenu_common(); |
|
317
|
style_submenu_element("Send Announcement","%R/announce"); |
|
318
|
style_set_current_feature("alerts"); |
|
319
|
style_header("Email Notification Setup"); |
|
320
|
@ <form action="%R/setup_notification" method="post"><div> |
|
321
|
@ <h1>Status   <input type="submit" name="submit" value="Refresh"></h1> |
|
322
|
@ </form> |
|
323
|
@ <table class="label-value"> |
|
324
|
if( alert_enabled() ){ |
|
325
|
stats_for_email(); |
|
326
|
}else{ |
|
327
|
@ <th>Disabled</th> |
|
328
|
} |
|
329
|
@ </table> |
|
330
|
@ <hr> |
|
331
|
@ <form action="%R/setup_notification" method="post"><div> |
|
332
|
@ <h1> Configuration </h1> |
|
333
|
@ <p><input type="submit" name="submit" value="Apply Changes"></p> |
|
334
|
@ <hr> |
|
335
|
login_insert_csrf_secret(); |
|
336
|
|
|
337
|
entry_attribute("Canonical Server URL", 40, "email-url", |
|
338
|
"eurl", "", 0); |
|
339
|
@ <p><b>Required.</b> |
|
340
|
@ This URL is used as the basename for hyperlinks included in |
|
341
|
@ email alert text. Omit the trailing "/". |
|
342
|
@ Suggested value: "%h(g.zBaseURL)" |
|
343
|
@ (Property: "email-url")</p> |
|
344
|
@ <hr> |
|
345
|
|
|
346
|
entry_attribute("Administrator email address", 40, "email-admin", |
|
347
|
"eadmin", "", 0); |
|
348
|
@ <p>This is the email for the human administrator for the system. |
|
349
|
@ Abuse and trouble reports and password reset requests are send here. |
|
350
|
@ (Property: "email-admin")</p> |
|
351
|
@ <hr> |
|
352
|
|
|
353
|
entry_attribute("\"Return-Path\" email address", 20, "email-self", |
|
354
|
"eself", "", 0); |
|
355
|
@ <p><b>Required.</b> |
|
356
|
@ This is the email to which email notification bounces should be sent. |
|
357
|
@ In cases where the email notification does not align with a specific |
|
358
|
@ Fossil login account (for example, digest messages), this is also |
|
359
|
@ the "From:" address of the email notification. |
|
360
|
@ The system administrator should arrange for emails sent to this address |
|
361
|
@ to be handed off to the "fossil email incoming" command so that Fossil |
|
362
|
@ can handle bounces. (Property: "email-self")</p> |
|
363
|
@ <hr> |
|
364
|
|
|
365
|
entry_attribute("List-ID", 40, "email-listid", |
|
366
|
"elistid", "", 0); |
|
367
|
@ <p> |
|
368
|
@ If this is not an empty string, then it becomes the argument to |
|
369
|
@ a "List-ID:" header on all out-bound notification emails. A list ID |
|
370
|
@ is required for the generation of unsubscribe links in notifications. |
|
371
|
@ (Property: "email-listid")</p> |
|
372
|
@ <hr> |
|
373
|
|
|
374
|
entry_attribute("Repository Nickname", 16, "email-subname", |
|
375
|
"enn", "", 0); |
|
376
|
@ <p><b>Required.</b> |
|
377
|
@ This is short name used to identifies the repository in the |
|
378
|
@ Subject: line of email alerts. Traditionally this name is |
|
379
|
@ included in square brackets. Examples: "[fossil-src]", "[sqlite-src]". |
|
380
|
@ (Property: "email-subname")</p> |
|
381
|
@ <hr> |
|
382
|
|
|
383
|
entry_attribute("Subscription Renewal Interval In Days", 8, |
|
384
|
"email-renew-interval", "eri", "", 0); |
|
385
|
@ <p> |
|
386
|
@ If this value is an integer N greater than or equal to 14, then email |
|
387
|
@ notification subscriptions will be suspended N days after the last known |
|
388
|
@ interaction with the user. This prevents sending notifications |
|
389
|
@ to abandoned accounts. If a subscription comes within 7 days of expiring, |
|
390
|
@ a separate email goes out with the daily digest that prompts the |
|
391
|
@ subscriber to click on a link to the "/renew" webpage in order to |
|
392
|
@ extend their subscription. Subscriptions never expire if this setting |
|
393
|
@ is less than 14 or is an empty string. |
|
394
|
@ (Property: "email-renew-interval")</p> |
|
395
|
@ <hr> |
|
396
|
|
|
397
|
multiple_choice_attribute("Email Send Method", "email-send-method", "esm", |
|
398
|
"off", count(azSendMethods)/2, azSendMethods); |
|
399
|
@ <p>How to send email. Requires auxiliary information from the fields |
|
400
|
@ that follow. Hint: Use the <a href="%R/announce">/announce</a> page |
|
401
|
@ to send test message to debug this setting. |
|
402
|
@ (Property: "email-send-method")</p> |
|
403
|
alert_schema(1); |
|
404
|
entry_attribute("SMTP Relay Host", 60, "email-send-relayhost", |
|
405
|
"esrh", "localhost", 0); |
|
406
|
@ <p>When the send method is "SMTP relay", each email message is |
|
407
|
@ transmitted via the SMTP protocol (rfc5321) to a "Mail Submission |
|
408
|
@ Agent" or "MSA" (rfc4409) at the hostname shown here. Optionally |
|
409
|
@ append a colon and TCP port number (ex: smtp.example.com:587). |
|
410
|
@ The default TCP port number is 25. |
|
411
|
@ Usage Hint: If Fossil is running inside of a chroot jail, then it might |
|
412
|
@ not be able to resolve hostnames. Work around this by using a raw IP |
|
413
|
@ address or create a "/etc/hosts" file inside the chroot jail. |
|
414
|
@ (Property: "email-send-relayhost")</p> |
|
415
|
@ |
|
416
|
entry_attribute("Store Emails In This Database", 60, "email-send-db", |
|
417
|
"esdb", "", 0); |
|
418
|
@ <p>When the send method is "store in a database", each email message is |
|
419
|
@ stored in an SQLite database file with the name given here. |
|
420
|
@ (Property: "email-send-db")</p> |
|
421
|
entry_attribute("Pipe Email Text Into This Command", 60, "email-send-command", |
|
422
|
"ecmd", "sendmail -ti", 0); |
|
423
|
@ <p>When the send method is "pipe to a command", this is the command |
|
424
|
@ that is run. Email messages are piped into the standard input of this |
|
425
|
@ command. The command is expected to extract the sender address, |
|
426
|
@ recipient addresses, and subject from the header of the piped email |
|
427
|
@ text. (Property: "email-send-command")</p> |
|
428
|
entry_attribute("Store Emails In This Directory", 60, "email-send-dir", |
|
429
|
"esdir", "", 0); |
|
430
|
@ <p>When the send method is "store in a directory", each email message is |
|
431
|
@ stored as a separate file in the directory shown here. |
|
432
|
@ (Property: "email-send-dir")</p> |
|
433
|
|
|
434
|
@ <hr> |
|
435
|
|
|
436
|
@ <p><input type="submit" name="submit" value="Apply Changes"></p> |
|
437
|
@ </div></form> |
|
438
|
db_end_transaction(0); |
|
439
|
style_finish_page(); |
|
440
|
} |
|
441
|
|
|
442
|
#if 0 |
|
443
|
/* |
|
444
|
** Encode pMsg as MIME base64 and append it to pOut |
|
445
|
*/ |
|
446
|
static void append_base64(Blob *pOut, Blob *pMsg){ |
|
447
|
int n, i, k; |
|
448
|
char zBuf[100]; |
|
449
|
n = blob_size(pMsg); |
|
450
|
for(i=0; i<n; i+=54){ |
|
451
|
k = translateBase64(blob_buffer(pMsg)+i, i+54<n ? 54 : n-i, zBuf); |
|
452
|
blob_append(pOut, zBuf, k); |
|
453
|
blob_append(pOut, "\r\n", 2); |
|
454
|
} |
|
455
|
} |
|
456
|
#endif |
|
457
|
|
|
458
|
/* |
|
459
|
** Encode pMsg using the quoted-printable email encoding and |
|
460
|
** append it onto pOut |
|
461
|
*/ |
|
462
|
static void append_quoted(Blob *pOut, Blob *pMsg){ |
|
463
|
char *zIn = blob_str(pMsg); |
|
464
|
char c; |
|
465
|
int iCol = 0; |
|
466
|
while( (c = *(zIn++))!=0 ){ |
|
467
|
if( (c>='!' && c<='~' && c!='=' && c!=':') |
|
468
|
|| (c==' ' && zIn[0]!='\r' && zIn[0]!='\n') |
|
469
|
){ |
|
470
|
blob_append_char(pOut, c); |
|
471
|
iCol++; |
|
472
|
if( iCol>=70 ){ |
|
473
|
blob_append(pOut, "=\r\n", 3); |
|
474
|
iCol = 0; |
|
475
|
} |
|
476
|
}else if( c=='\r' && zIn[0]=='\n' ){ |
|
477
|
zIn++; |
|
478
|
blob_append(pOut, "\r\n", 2); |
|
479
|
iCol = 0; |
|
480
|
}else if( c=='\n' ){ |
|
481
|
blob_append(pOut, "\r\n", 2); |
|
482
|
iCol = 0; |
|
483
|
}else{ |
|
484
|
char x[3]; |
|
485
|
x[0] = '='; |
|
486
|
x[1] = "0123456789ABCDEF"[(c>>4)&0xf]; |
|
487
|
x[2] = "0123456789ABCDEF"[c&0xf]; |
|
488
|
blob_append(pOut, x, 3); |
|
489
|
iCol += 3; |
|
490
|
} |
|
491
|
} |
|
492
|
} |
|
493
|
|
|
494
|
#if INTERFACE |
|
495
|
/* |
|
496
|
** An instance of the following object is used to send emails. |
|
497
|
*/ |
|
498
|
struct AlertSender { |
|
499
|
sqlite3 *db; /* Database emails are sent to */ |
|
500
|
sqlite3_stmt *pStmt; /* Stmt to insert into the database */ |
|
501
|
const char *zDest; /* How to send email. */ |
|
502
|
const char *zDb; /* Name of database file */ |
|
503
|
const char *zDir; /* Directory in which to store as email files */ |
|
504
|
const char *zCmd; /* Command to run for each email */ |
|
505
|
const char *zFrom; /* Emails come from here */ |
|
506
|
const char *zListId; /* Argument to List-ID header */ |
|
507
|
SmtpSession *pSmtp; /* SMTP relay connection */ |
|
508
|
Blob out; /* For zDest=="blob" */ |
|
509
|
char *zErr; /* Error message */ |
|
510
|
u32 mFlags; /* Flags */ |
|
511
|
int bImmediateFail; /* On any error, call fossil_fatal() */ |
|
512
|
}; |
|
513
|
|
|
514
|
/* Allowed values for mFlags to alert_sender_new(). |
|
515
|
*/ |
|
516
|
#define ALERT_IMMEDIATE_FAIL 0x0001 /* Call fossil_fatal() on any error */ |
|
517
|
#define ALERT_TRACE 0x0002 /* Log sending process on console */ |
|
518
|
|
|
519
|
#endif /* INTERFACE */ |
|
520
|
|
|
521
|
/* |
|
522
|
** Shutdown an emailer. Clear all information other than the error message. |
|
523
|
*/ |
|
524
|
static void emailerShutdown(AlertSender *p){ |
|
525
|
sqlite3_finalize(p->pStmt); |
|
526
|
p->pStmt = 0; |
|
527
|
sqlite3_close(p->db); |
|
528
|
p->db = 0; |
|
529
|
p->zDb = 0; |
|
530
|
p->zDir = 0; |
|
531
|
p->zCmd = 0; |
|
532
|
p->zListId = 0; |
|
533
|
if( p->pSmtp ){ |
|
534
|
smtp_client_quit(p->pSmtp); |
|
535
|
smtp_session_free(p->pSmtp); |
|
536
|
p->pSmtp = 0; |
|
537
|
} |
|
538
|
blob_reset(&p->out); |
|
539
|
} |
|
540
|
|
|
541
|
/* |
|
542
|
** Put the AlertSender into an error state. |
|
543
|
*/ |
|
544
|
static void emailerError(AlertSender *p, const char *zFormat, ...){ |
|
545
|
va_list ap; |
|
546
|
fossil_free(p->zErr); |
|
547
|
va_start(ap, zFormat); |
|
548
|
p->zErr = vmprintf(zFormat, ap); |
|
549
|
va_end(ap); |
|
550
|
emailerShutdown(p); |
|
551
|
if( p->mFlags & ALERT_IMMEDIATE_FAIL ){ |
|
552
|
fossil_fatal("%s", p->zErr); |
|
553
|
} |
|
554
|
} |
|
555
|
|
|
556
|
/* |
|
557
|
** Free an email sender object |
|
558
|
*/ |
|
559
|
void alert_sender_free(AlertSender *p){ |
|
560
|
if( p ){ |
|
561
|
emailerShutdown(p); |
|
562
|
fossil_free(p->zErr); |
|
563
|
fossil_free(p); |
|
564
|
} |
|
565
|
} |
|
566
|
|
|
567
|
/* |
|
568
|
** Get an email setting value. Report an error if not configured. |
|
569
|
** Return 0 on success and one if there is an error. |
|
570
|
*/ |
|
571
|
static int emailerGetSetting( |
|
572
|
AlertSender *p, /* Where to report the error */ |
|
573
|
const char **pzVal, /* Write the setting value here */ |
|
574
|
const char *zName /* Name of the setting */ |
|
575
|
){ |
|
576
|
const char *z = db_get(zName, 0); |
|
577
|
int rc = 0; |
|
578
|
if( z==0 || z[0]==0 ){ |
|
579
|
emailerError(p, "missing \"%s\" setting", zName); |
|
580
|
rc = 1; |
|
581
|
}else{ |
|
582
|
*pzVal = z; |
|
583
|
} |
|
584
|
return rc; |
|
585
|
} |
|
586
|
|
|
587
|
/* |
|
588
|
** Create a new AlertSender object. |
|
589
|
** |
|
590
|
** The method used for sending email is determined by various email-* |
|
591
|
** settings, and especially email-send-method. The repository |
|
592
|
** email-send-method can be overridden by the zAltDest argument to |
|
593
|
** cause a different sending mechanism to be used. Pass "stdout" to |
|
594
|
** zAltDest to cause all emails to be printed to the console for |
|
595
|
** debugging purposes. |
|
596
|
** |
|
597
|
** The AlertSender object returned must be freed using alert_sender_free(). |
|
598
|
*/ |
|
599
|
AlertSender *alert_sender_new(const char *zAltDest, u32 mFlags){ |
|
600
|
AlertSender *p; |
|
601
|
|
|
602
|
p = fossil_malloc(sizeof(*p)); |
|
603
|
memset(p, 0, sizeof(*p)); |
|
604
|
blob_init(&p->out, 0, 0); |
|
605
|
p->mFlags = mFlags; |
|
606
|
if( zAltDest ){ |
|
607
|
p->zDest = zAltDest; |
|
608
|
}else{ |
|
609
|
p->zDest = db_get("email-send-method",0); |
|
610
|
} |
|
611
|
if( fossil_strcmp(p->zDest,"off")==0 ) return p; |
|
612
|
if( emailerGetSetting(p, &p->zFrom, "email-self") ) return p; |
|
613
|
p->zListId = db_get("email-listid", 0); |
|
614
|
if( fossil_strcmp(p->zDest,"db")==0 ){ |
|
615
|
char *zErr; |
|
616
|
int rc; |
|
617
|
if( emailerGetSetting(p, &p->zDb, "email-send-db") ) return p; |
|
618
|
rc = sqlite3_open(p->zDb, &p->db); |
|
619
|
if( rc ){ |
|
620
|
emailerError(p, "unable to open output database file \"%s\": %s", |
|
621
|
p->zDb, sqlite3_errmsg(p->db)); |
|
622
|
return p; |
|
623
|
} |
|
624
|
rc = sqlite3_exec(p->db, "CREATE TABLE IF NOT EXISTS email(\n" |
|
625
|
" emailid INTEGER PRIMARY KEY,\n" |
|
626
|
" msg TEXT\n);", 0, 0, &zErr); |
|
627
|
if( zErr ){ |
|
628
|
emailerError(p, "CREATE TABLE failed with \"%s\"", zErr); |
|
629
|
sqlite3_free(zErr); |
|
630
|
return p; |
|
631
|
} |
|
632
|
rc = sqlite3_prepare_v2(p->db, "INSERT INTO email(msg) VALUES(?1)", -1, |
|
633
|
&p->pStmt, 0); |
|
634
|
if( rc ){ |
|
635
|
emailerError(p, "cannot prepare INSERT statement: %s", |
|
636
|
sqlite3_errmsg(p->db)); |
|
637
|
return p; |
|
638
|
} |
|
639
|
}else if( fossil_strcmp(p->zDest, "pipe")==0 ){ |
|
640
|
emailerGetSetting(p, &p->zCmd, "email-send-command"); |
|
641
|
}else if( fossil_strcmp(p->zDest, "dir")==0 ){ |
|
642
|
emailerGetSetting(p, &p->zDir, "email-send-dir"); |
|
643
|
}else if( fossil_strcmp(p->zDest, "blob")==0 ){ |
|
644
|
blob_init(&p->out, 0, 0); |
|
645
|
}else if( fossil_strcmp(p->zDest, "relay")==0 |
|
646
|
|| fossil_strcmp(p->zDest, "debug-relay")==0 |
|
647
|
){ |
|
648
|
const char *zRelay = 0; |
|
649
|
emailerGetSetting(p, &zRelay, "email-send-relayhost"); |
|
650
|
if( zRelay ){ |
|
651
|
u32 smtpFlags = SMTP_DIRECT; |
|
652
|
if( mFlags & ALERT_TRACE ) smtpFlags |= SMTP_TRACE_STDOUT; |
|
653
|
blob_init(&p->out, 0, 0); |
|
654
|
p->pSmtp = smtp_session_new(domain_of_addr(p->zFrom), zRelay, |
|
655
|
smtpFlags, 0); |
|
656
|
if( p->pSmtp==0 || p->pSmtp->zErr ){ |
|
657
|
emailerError(p, "Could not start SMTP session: %s", |
|
658
|
p->pSmtp ? p->pSmtp->zErr : "reason unknown"); |
|
659
|
}else if( p->zDest[0]=='d' ){ |
|
660
|
smtp_session_config(p->pSmtp, SMTP_TRACE_BLOB, &p->out); |
|
661
|
} |
|
662
|
} |
|
663
|
} |
|
664
|
return p; |
|
665
|
} |
|
666
|
|
|
667
|
/* |
|
668
|
** Scan the header of the email message in pMsg looking for the |
|
669
|
** (first) occurrence of zField. Fill pValue with the content of |
|
670
|
** that field. |
|
671
|
** |
|
672
|
** This routine initializes pValue. Any prior content of pValue is |
|
673
|
** discarded (leaked). |
|
674
|
** |
|
675
|
** Return non-zero on success. Return 0 if no instance of the header |
|
676
|
** is found. |
|
677
|
*/ |
|
678
|
int email_header_value(Blob *pMsg, const char *zField, Blob *pValue){ |
|
679
|
int nField = (int)strlen(zField); |
|
680
|
Blob line; |
|
681
|
blob_rewind(pMsg); |
|
682
|
blob_init(pValue,0,0); |
|
683
|
while( blob_line(pMsg, &line) ){ |
|
684
|
int n, i; |
|
685
|
char *z; |
|
686
|
blob_trim(&line); |
|
687
|
n = blob_size(&line); |
|
688
|
if( n==0 ) return 0; |
|
689
|
if( n<nField+1 ) continue; |
|
690
|
z = blob_buffer(&line); |
|
691
|
if( sqlite3_strnicmp(z, zField, nField)==0 && z[nField]==':' ){ |
|
692
|
for(i=nField+1; i<n && fossil_isspace(z[i]); i++){} |
|
693
|
blob_init(pValue, z+i, n-i); |
|
694
|
while( blob_line(pMsg, &line) ){ |
|
695
|
blob_trim(&line); |
|
696
|
n = blob_size(&line); |
|
697
|
if( n==0 ) break; |
|
698
|
z = blob_buffer(&line); |
|
699
|
if( !fossil_isspace(z[0]) ) break; |
|
700
|
for(i=1; i<n && fossil_isspace(z[i]); i++){} |
|
701
|
blob_append(pValue, " ", 1); |
|
702
|
blob_append(pValue, z+i, n-i); |
|
703
|
} |
|
704
|
return 1; |
|
705
|
} |
|
706
|
} |
|
707
|
return 0; |
|
708
|
} |
|
709
|
|
|
710
|
/* |
|
711
|
** Determine whether or not the input string is a valid email address. |
|
712
|
** Only look at character up to but not including the first \000 or |
|
713
|
** the first cTerm character, whichever comes first. |
|
714
|
** |
|
715
|
** Return the length of the email address string in bytes if the email |
|
716
|
** address is valid. If the email address is misformed, return 0. |
|
717
|
*/ |
|
718
|
int email_address_is_valid(const char *z, char cTerm){ |
|
719
|
int i; |
|
720
|
int nAt = 0; |
|
721
|
int nDot = 0; |
|
722
|
char c; |
|
723
|
if( z[0]=='.' ) return 0; /* Local part cannot begin with "." */ |
|
724
|
for(i=0; (c = z[i])!=0 && c!=cTerm; i++){ |
|
725
|
if( fossil_isalnum(c) ){ |
|
726
|
/* Alphanumerics are always ok */ |
|
727
|
}else if( c=='@' ){ |
|
728
|
if( nAt ) return 0; /* Only a single "@" allowed */ |
|
729
|
if( i>64 ) return 0; /* Local part too big */ |
|
730
|
nAt = 1; |
|
731
|
nDot = 0; |
|
732
|
if( i==0 ) return 0; /* Disallow empty local part */ |
|
733
|
if( z[i-1]=='.' ) return 0; /* Last char of local cannot be "." */ |
|
734
|
if( z[i+1]=='.' || z[i+1]=='-' ){ |
|
735
|
return 0; /* Domain cannot begin with "." or "-" */ |
|
736
|
} |
|
737
|
}else if( c=='-' ){ |
|
738
|
if( z[i+1]==cTerm ) return 0; /* Last character cannot be "-" */ |
|
739
|
}else if( c=='.' ){ |
|
740
|
if( z[i+1]=='.' ) return 0; /* Do not allow ".." */ |
|
741
|
if( z[i+1]==cTerm ) return 0; /* Domain may not end with . */ |
|
742
|
nDot++; |
|
743
|
}else if( (c=='_' || c=='+') && nAt==0 ){ |
|
744
|
/* _ and + are ok in the local part */ |
|
745
|
}else{ |
|
746
|
return 0; /* Anything else is an error */ |
|
747
|
} |
|
748
|
} |
|
749
|
if( c!=cTerm ) return 0; /* Missing terminator */ |
|
750
|
if( nAt==0 ) return 0; /* No "@" found anywhere */ |
|
751
|
if( nDot==0 ) return 0; /* No "." in the domain */ |
|
752
|
return i; |
|
753
|
} |
|
754
|
|
|
755
|
/* |
|
756
|
** Make a copy of the input string up to but not including the |
|
757
|
** first cTerm character. |
|
758
|
** |
|
759
|
** Verify that the string to be copied really is a valid |
|
760
|
** email address. If it is not, then return NULL. |
|
761
|
** |
|
762
|
** This routine is more restrictive than necessary. It does not |
|
763
|
** allow comments, IP address, quoted strings, or certain uncommon |
|
764
|
** characters. The only non-alphanumerics allowed in the local |
|
765
|
** part are "_", "+", "-" and "+". |
|
766
|
*/ |
|
767
|
char *email_copy_addr(const char *z, char cTerm ){ |
|
768
|
int i = email_address_is_valid(z, cTerm); |
|
769
|
return i==0 ? 0 : mprintf("%.*s", i, z); |
|
770
|
} |
|
771
|
|
|
772
|
/* |
|
773
|
** Scan the input string for a valid email address that may be |
|
774
|
** enclosed in <...>, or delimited by ',' or ':' or '=' or ' '. |
|
775
|
** If the string contains one or more email addresses, extract the first |
|
776
|
** one into memory obtained from mprintf() and return a pointer to it. |
|
777
|
** If no valid email address can be found, return NULL. |
|
778
|
*/ |
|
779
|
char *alert_find_emailaddr(const char *zIn){ |
|
780
|
char *zOut = 0; |
|
781
|
do{ |
|
782
|
zOut = email_copy_addr(zIn, zIn[strcspn(zIn, ">,:= ")]); |
|
783
|
if( zOut!=0 ) break; |
|
784
|
zIn = (const char *)strpbrk(zIn, "<,:= "); |
|
785
|
if( zIn==0 ) break; |
|
786
|
zIn++; |
|
787
|
}while( zIn!=0 ); |
|
788
|
return zOut; |
|
789
|
} |
|
790
|
|
|
791
|
/* |
|
792
|
** SQL function: find_emailaddr(X) |
|
793
|
** |
|
794
|
** Return the first valid email address of the form <...> in input string |
|
795
|
** X. Or return NULL if not found. |
|
796
|
*/ |
|
797
|
void alert_find_emailaddr_func( |
|
798
|
sqlite3_context *context, |
|
799
|
int argc, |
|
800
|
sqlite3_value **argv |
|
801
|
){ |
|
802
|
const char *zIn = (const char*)sqlite3_value_text(argv[0]); |
|
803
|
char *zOut = alert_find_emailaddr(zIn); |
|
804
|
if( zOut ){ |
|
805
|
sqlite3_result_text(context, zOut, -1, fossil_free); |
|
806
|
} |
|
807
|
} |
|
808
|
|
|
809
|
/* |
|
810
|
** SQL function: display_name(X) |
|
811
|
** |
|
812
|
** If X is a string, search for a user name at the beginning of that |
|
813
|
** string. The user name must be followed by an email address. If |
|
814
|
** found, return the user name. If not found, return NULL. |
|
815
|
** |
|
816
|
** This routine is used to extract the display name from the USER.INFO |
|
817
|
** field. |
|
818
|
*/ |
|
819
|
void alert_display_name_func( |
|
820
|
sqlite3_context *context, |
|
821
|
int argc, |
|
822
|
sqlite3_value **argv |
|
823
|
){ |
|
824
|
const char *zIn = (const char*)sqlite3_value_text(argv[0]); |
|
825
|
int i; |
|
826
|
if( zIn==0 ) return; |
|
827
|
while( fossil_isspace(zIn[0]) ) zIn++; |
|
828
|
for(i=0; zIn[i] && zIn[i]!='<' && zIn[i]!='\n'; i++){} |
|
829
|
if( zIn[i]=='<' ){ |
|
830
|
while( i>0 && fossil_isspace(zIn[i-1]) ){ i--; } |
|
831
|
if( i>0 ){ |
|
832
|
sqlite3_result_text(context, zIn, i, SQLITE_TRANSIENT); |
|
833
|
} |
|
834
|
} |
|
835
|
} |
|
836
|
|
|
837
|
/* |
|
838
|
** Return the hostname portion of an email address - the part following |
|
839
|
** the @ |
|
840
|
*/ |
|
841
|
char *alert_hostname(const char *zAddr){ |
|
842
|
char *z = strchr(zAddr, '@'); |
|
843
|
if( z ){ |
|
844
|
z++; |
|
845
|
}else{ |
|
846
|
z = (char*)zAddr; |
|
847
|
} |
|
848
|
return z; |
|
849
|
} |
|
850
|
|
|
851
|
/* |
|
852
|
** Return a pointer to a fake email mailbox name that corresponds |
|
853
|
** to human-readable name zFromName. The fake mailbox name is based |
|
854
|
** on a hash. No huge problems arise if there is a hash collisions, |
|
855
|
** but it is still better if collisions can be avoided. |
|
856
|
** |
|
857
|
** The returned string is held in a static buffer and is overwritten |
|
858
|
** by each subsequent call to this routine. |
|
859
|
*/ |
|
860
|
static char *alert_mailbox_name(const char *zFromName){ |
|
861
|
static char zHash[20]; |
|
862
|
unsigned int x = 0; |
|
863
|
int n = 0; |
|
864
|
while( zFromName[0] ){ |
|
865
|
n++; |
|
866
|
x = x*1103515245 + 12345 + ((unsigned char*)zFromName)[0]; |
|
867
|
zFromName++; |
|
868
|
} |
|
869
|
sqlite3_snprintf(sizeof(zHash), zHash, |
|
870
|
"noreply%x%08x", n, x); |
|
871
|
return zHash; |
|
872
|
} |
|
873
|
|
|
874
|
/* |
|
875
|
** COMMAND: test-mailbox-hashname |
|
876
|
** |
|
877
|
** Usage: %fossil test-mailbox-hashname HUMAN-NAME ... |
|
878
|
** |
|
879
|
** Return the mailbox hash name corresponding to each human-readable |
|
880
|
** name on the command line. This is a test interface for the |
|
881
|
** alert_mailbox_name() function. |
|
882
|
*/ |
|
883
|
void alert_test_mailbox_hashname(void){ |
|
884
|
int i; |
|
885
|
for(i=2; i<g.argc; i++){ |
|
886
|
fossil_print("%30s: %s\n", g.argv[i], alert_mailbox_name(g.argv[i])); |
|
887
|
} |
|
888
|
} |
|
889
|
|
|
890
|
/* |
|
891
|
** Extract all To: header values from the email header supplied. |
|
892
|
** Store them in the array list. |
|
893
|
*/ |
|
894
|
void email_header_to(Blob *pMsg, int *pnTo, char ***pazTo){ |
|
895
|
int nTo = 0; |
|
896
|
char **azTo = 0; |
|
897
|
Blob v; |
|
898
|
char *z, *zAddr; |
|
899
|
int i; |
|
900
|
|
|
901
|
email_header_value(pMsg, "to", &v); |
|
902
|
z = blob_str(&v); |
|
903
|
for(i=0; z[i]; i++){ |
|
904
|
if( z[i]=='<' && (zAddr = email_copy_addr(&z[i+1],'>'))!=0 ){ |
|
905
|
azTo = fossil_realloc(azTo, sizeof(azTo[0])*(nTo+1) ); |
|
906
|
azTo[nTo++] = zAddr; |
|
907
|
} |
|
908
|
} |
|
909
|
*pnTo = nTo; |
|
910
|
*pazTo = azTo; |
|
911
|
} |
|
912
|
|
|
913
|
/* |
|
914
|
** Free a list of To addresses obtained from a prior call to |
|
915
|
** email_header_to() |
|
916
|
*/ |
|
917
|
void email_header_to_free(int nTo, char **azTo){ |
|
918
|
int i; |
|
919
|
for(i=0; i<nTo; i++) fossil_free(azTo[i]); |
|
920
|
fossil_free(azTo); |
|
921
|
} |
|
922
|
|
|
923
|
/* |
|
924
|
** Send a single email message. |
|
925
|
** |
|
926
|
** The recipient(s) must be specified using "To:" or "Cc:" or "Bcc:" fields |
|
927
|
** in the header. Likewise, the header must contains a "Subject:" line. |
|
928
|
** The header might also include fields like "Message-Id:" or |
|
929
|
** "In-Reply-To:". |
|
930
|
** |
|
931
|
** This routine will add fields to the header as follows: |
|
932
|
** |
|
933
|
** From: |
|
934
|
** Date: |
|
935
|
** Message-Id: |
|
936
|
** Content-Type: |
|
937
|
** Content-Transfer-Encoding: |
|
938
|
** MIME-Version: |
|
939
|
** Sender: |
|
940
|
** |
|
941
|
** The caller maintains ownership of the input Blobs. This routine will |
|
942
|
** read the Blobs and send them onward to the email system, but it will |
|
943
|
** not free them. |
|
944
|
** |
|
945
|
** The Message-Id: field is added if there is not already a Message-Id |
|
946
|
** in the pHdr parameter. |
|
947
|
** |
|
948
|
** If the zFromName argument is not NULL, then it should be a human-readable |
|
949
|
** name or handle for the sender. In that case, "From:" becomes a made-up |
|
950
|
** email address based on a hash of zFromName and the domain of email-self, |
|
951
|
** and an additional "Sender:" field is inserted with the email-self |
|
952
|
** address. Downstream software might use the Sender header to set |
|
953
|
** the envelope-from address of the email. If zFromName is a NULL pointer, |
|
954
|
** then the "From:" is set to the email-self value and Sender is |
|
955
|
** omitted. |
|
956
|
*/ |
|
957
|
void alert_send( |
|
958
|
AlertSender *p, /* Emailer context */ |
|
959
|
Blob *pHdr, /* Email header (incomplete) */ |
|
960
|
Blob *pBody, /* Email body */ |
|
961
|
const char *zFromName /* Optional human-readable name of sender */ |
|
962
|
){ |
|
963
|
Blob all, *pOut; |
|
964
|
u64 r1, r2; |
|
965
|
if( p->mFlags & ALERT_TRACE ){ |
|
966
|
fossil_print("Sending email\n"); |
|
967
|
} |
|
968
|
if( fossil_strcmp(p->zDest, "off")==0 ){ |
|
969
|
return; |
|
970
|
} |
|
971
|
blob_init(&all, 0, 0); |
|
972
|
if( fossil_strcmp(p->zDest, "blob")==0 ){ |
|
973
|
pOut = &p->out; |
|
974
|
if( blob_size(pOut) ){ |
|
975
|
blob_appendf(pOut, "%.72c\n", '='); |
|
976
|
} |
|
977
|
}else{ |
|
978
|
pOut = &all; |
|
979
|
} |
|
980
|
blob_append(pOut, blob_buffer(pHdr), blob_size(pHdr)); |
|
981
|
if( p->zFrom==0 || p->zFrom[0]==0 ){ |
|
982
|
return; /* email-self is not set. Error will be reported separately */ |
|
983
|
}else if( zFromName ){ |
|
984
|
blob_appendf(pOut, "From: %s <%s@%s>\r\n", |
|
985
|
zFromName, alert_mailbox_name(zFromName), alert_hostname(p->zFrom)); |
|
986
|
blob_appendf(pOut, "Sender: <%s>\r\n", p->zFrom); |
|
987
|
}else{ |
|
988
|
blob_appendf(pOut, "From: <%s>\r\n", p->zFrom); |
|
989
|
} |
|
990
|
blob_appendf(pOut, "Date: %z\r\n", cgi_rfc822_datestamp(time(0))); |
|
991
|
if( strstr(blob_str(pHdr), "\r\nMessage-Id:")==0 ){ |
|
992
|
/* Message-id format: "<$(date)x$(random)@$(from-host)>" where $(date) is |
|
993
|
** the current unix-time in hex, $(random) is a 64-bit random number, |
|
994
|
** and $(from) is the domain part of the email-self setting. */ |
|
995
|
sqlite3_randomness(sizeof(r1), &r1); |
|
996
|
r2 = time(0); |
|
997
|
blob_appendf(pOut, "Message-Id: <%llxx%016llx@%s>\r\n", |
|
998
|
r2, r1, alert_hostname(p->zFrom)); |
|
999
|
} |
|
1000
|
blob_add_final_newline(pBody); |
|
1001
|
blob_appendf(pOut, "MIME-Version: 1.0\r\n"); |
|
1002
|
blob_appendf(pOut, "Content-Type: text/plain; charset=\"UTF-8\"\r\n"); |
|
1003
|
#if 0 |
|
1004
|
blob_appendf(pOut, "Content-Transfer-Encoding: base64\r\n\r\n"); |
|
1005
|
append_base64(pOut, pBody); |
|
1006
|
#else |
|
1007
|
blob_appendf(pOut, "Content-Transfer-Encoding: quoted-printable\r\n\r\n"); |
|
1008
|
append_quoted(pOut, pBody); |
|
1009
|
#endif |
|
1010
|
if( p->pStmt ){ |
|
1011
|
int i, rc; |
|
1012
|
sqlite3_bind_text(p->pStmt, 1, blob_str(&all), -1, SQLITE_TRANSIENT); |
|
1013
|
for(i=0; i<100 && sqlite3_step(p->pStmt)==SQLITE_BUSY; i++){ |
|
1014
|
sqlite3_sleep(10); |
|
1015
|
} |
|
1016
|
rc = sqlite3_reset(p->pStmt); |
|
1017
|
if( rc!=SQLITE_OK ){ |
|
1018
|
emailerError(p, "Failed to insert email message into output queue.\n" |
|
1019
|
"%s", sqlite3_errmsg(p->db)); |
|
1020
|
} |
|
1021
|
}else if( p->zCmd ){ |
|
1022
|
FILE *out = popen(p->zCmd, "w"); |
|
1023
|
if( out ){ |
|
1024
|
fwrite(blob_buffer(&all), 1, blob_size(&all), out); |
|
1025
|
pclose(out); |
|
1026
|
}else{ |
|
1027
|
emailerError(p, "Could not open output pipe \"%s\"", p->zCmd); |
|
1028
|
} |
|
1029
|
}else if( p->zDir ){ |
|
1030
|
char *zFile = file_time_tempname(p->zDir, ".email"); |
|
1031
|
blob_write_to_file(&all, zFile); |
|
1032
|
fossil_free(zFile); |
|
1033
|
}else if( p->pSmtp ){ |
|
1034
|
char **azTo = 0; |
|
1035
|
int nTo = 0; |
|
1036
|
SmtpSession *pSmtp = p->pSmtp; |
|
1037
|
email_header_to(pHdr, &nTo, &azTo); |
|
1038
|
if( nTo>0 && !pSmtp->bFatal ){ |
|
1039
|
smtp_send_msg(pSmtp,p->zFrom,nTo,(const char**)azTo,blob_str(&all)); |
|
1040
|
if( pSmtp->zErr && !pSmtp->bFatal ){ |
|
1041
|
smtp_send_msg(pSmtp,p->zFrom,nTo,(const char**)azTo,blob_str(&all)); |
|
1042
|
} |
|
1043
|
if( pSmtp->zErr ){ |
|
1044
|
fossil_errorlog("SMTP: (%s) %s", pSmtp->bFatal ? "fatal" : "retry", |
|
1045
|
pSmtp->zErr); |
|
1046
|
} |
|
1047
|
email_header_to_free(nTo, azTo); |
|
1048
|
} |
|
1049
|
}else if( strcmp(p->zDest, "stdout")==0 ){ |
|
1050
|
char **azTo = 0; |
|
1051
|
int nTo = 0; |
|
1052
|
int i; |
|
1053
|
email_header_to(pHdr, &nTo, &azTo); |
|
1054
|
for(i=0; i<nTo; i++){ |
|
1055
|
fossil_print("X-To-Test-%d: [%s]\r\n", i, azTo[i]); |
|
1056
|
} |
|
1057
|
email_header_to_free(nTo, azTo); |
|
1058
|
blob_add_final_newline(&all); |
|
1059
|
fossil_print("%s", blob_str(&all)); |
|
1060
|
} |
|
1061
|
blob_reset(&all); |
|
1062
|
} |
|
1063
|
|
|
1064
|
/* |
|
1065
|
** SETTING: email-url width=40 |
|
1066
|
** This is the main URL used to access the repository for cloning or |
|
1067
|
** syncing or for operating the web interface. It is also |
|
1068
|
** the basename for hyperlinks included in email alert text. |
|
1069
|
** Omit the trailing "/". If the repository is not intended to be |
|
1070
|
** a long-running server and will not be sending email notifications, |
|
1071
|
** then leave this setting blank. |
|
1072
|
*/ |
|
1073
|
/* |
|
1074
|
** SETTING: email-admin width=40 |
|
1075
|
** This is the email address for the human administrator for the system. |
|
1076
|
** Abuse and trouble reports and password reset requests are send here. |
|
1077
|
*/ |
|
1078
|
/* |
|
1079
|
** SETTING: email-subname width=16 |
|
1080
|
** This is a short name used to identifies the repository in the Subject: |
|
1081
|
** line of email alerts. Traditionally this name is included in square |
|
1082
|
** brackets. Examples: "[fossil-src]", "[sqlite-src]". |
|
1083
|
*/ |
|
1084
|
/* |
|
1085
|
** SETTING: email-renew-interval width=16 |
|
1086
|
** If this setting as an integer N that is 14 or greater then email |
|
1087
|
** notification is suspended for subscriptions that have a "last contact |
|
1088
|
** time" of more than N days ago. The "last contact time" is recorded |
|
1089
|
** in the SUBSCRIBER.LASTCONTACT entry of the database. Logging in, |
|
1090
|
** sending a forum post, editing a wiki page, changing subscription settings |
|
1091
|
** at /alerts, or visiting /renew all update the last contact time. |
|
1092
|
** If this setting is not an integer value or is less than 14 or undefined, |
|
1093
|
** then subscriptions never expire. |
|
1094
|
*/ |
|
1095
|
/* X-VARIABLE: email-renew-warning |
|
1096
|
** X-VARIABLE: email-renew-cutoff |
|
1097
|
** |
|
1098
|
** These CONFIG table entries are not considered "settings" since their |
|
1099
|
** values are computed and updated automatically. |
|
1100
|
** |
|
1101
|
** email-renew-cutoff is the lastContact cutoff for subscription. It |
|
1102
|
** is measured in days since 1970-01-01. If The lastContact time for |
|
1103
|
** a subscription is less than email-renew-cutoff, then now new emails |
|
1104
|
** are sent to the subscriber. |
|
1105
|
** |
|
1106
|
** email-renew-warning is the time (in days since 1970-01-01) when the |
|
1107
|
** last batch of "your subscription is about to expire" emails were |
|
1108
|
** sent out. |
|
1109
|
** |
|
1110
|
** email-renew-cutoff is normally 7 days behind email-renew-warning. |
|
1111
|
*/ |
|
1112
|
/* |
|
1113
|
** SETTING: email-send-method width=5 default=off sensitive |
|
1114
|
** Determine the method used to send email. Allowed values are |
|
1115
|
** "off", "relay", "pipe", "dir", "db", and "stdout". The "off" value |
|
1116
|
** means no email is ever sent. The "relay" value means emails are sent |
|
1117
|
** to an Mail Sending Agent using SMTP located at email-send-relayhost. |
|
1118
|
** The "pipe" value means email messages are piped into a command |
|
1119
|
** determined by the email-send-command setting. The "dir" value means |
|
1120
|
** emails are written to individual files in a directory determined |
|
1121
|
** by the email-send-dir setting. The "db" value means that emails |
|
1122
|
** are added to an SQLite database named by the* email-send-db setting. |
|
1123
|
** The "stdout" value writes email text to standard output, for debugging. |
|
1124
|
*/ |
|
1125
|
/* |
|
1126
|
** SETTING: email-send-command width=40 sensitive |
|
1127
|
** This is a command to which outbound email content is piped when the |
|
1128
|
** email-send-method is set to "pipe". The command must extract |
|
1129
|
** recipient, sender, subject, and all other relevant information |
|
1130
|
** from the email header. |
|
1131
|
*/ |
|
1132
|
/* |
|
1133
|
** SETTING: email-send-dir width=40 sensitive |
|
1134
|
** This is a directory into which outbound emails are written as individual |
|
1135
|
** files if the email-send-method is set to "dir". |
|
1136
|
*/ |
|
1137
|
/* |
|
1138
|
** SETTING: email-send-db width=40 sensitive |
|
1139
|
** This is an SQLite database file into which outbound emails are written |
|
1140
|
** if the email-send-method is set to "db". |
|
1141
|
*/ |
|
1142
|
/* |
|
1143
|
** SETTING: email-self width=40 |
|
1144
|
** This is the email address for the repository. Outbound emails add |
|
1145
|
** this email address as the "From:" field. |
|
1146
|
*/ |
|
1147
|
/* |
|
1148
|
** SETTING: email-listid width=40 |
|
1149
|
** If this setting is not an empty string, then it becomes the argument to |
|
1150
|
** a "List-ID:" header that is added to all out-bound notification emails. |
|
1151
|
** A list ID is required for the generation of unsubscribe links in |
|
1152
|
** notifications. |
|
1153
|
*/ |
|
1154
|
/* |
|
1155
|
** SETTING: email-send-relayhost width=40 sensitive default=127.0.0.1 |
|
1156
|
** This is the hostname and TCP port to which output email messages |
|
1157
|
** are sent when email-send-method is "relay". There should be an |
|
1158
|
** SMTP server configured as a Mail Submission Agent listening on the |
|
1159
|
** designated host and port and all times. |
|
1160
|
*/ |
|
1161
|
|
|
1162
|
|
|
1163
|
/* |
|
1164
|
** COMMAND: alerts* abbrv-subcom |
|
1165
|
** |
|
1166
|
** Usage: %fossil alerts SUBCOMMAND ARGS... |
|
1167
|
** |
|
1168
|
** Subcommands: |
|
1169
|
** |
|
1170
|
** pending Show all pending alerts. Useful for debugging. |
|
1171
|
** |
|
1172
|
** reset Hard reset of all email notification tables |
|
1173
|
** in the repository. This erases all subscription |
|
1174
|
** information. ** Use with extreme care ** |
|
1175
|
** |
|
1176
|
** send Compose and send pending email alerts. |
|
1177
|
** Some installations may want to do this via |
|
1178
|
** a cron-job to make sure alerts are sent |
|
1179
|
** in a timely manner. |
|
1180
|
** |
|
1181
|
** Options: |
|
1182
|
** --digest Send digests |
|
1183
|
** --renewal Send subscription renewal |
|
1184
|
** notices |
|
1185
|
** --test Write to standard output |
|
1186
|
** |
|
1187
|
** settings [NAME VALUE] With no arguments, list all email settings. |
|
1188
|
** Or change the value of a single email setting. |
|
1189
|
** |
|
1190
|
** status Report on the status of the email alert |
|
1191
|
** subsystem |
|
1192
|
** |
|
1193
|
** subscribers [PATTERN] List all subscribers matching PATTERN. Either |
|
1194
|
** LIKE or GLOB wildcards can be used in PATTERN. |
|
1195
|
** |
|
1196
|
** test-message TO [OPTS] Send a single email message using whatever |
|
1197
|
** email sending mechanism is currently configured. |
|
1198
|
** Use this for testing the email notification |
|
1199
|
** configuration. |
|
1200
|
** |
|
1201
|
** Options: |
|
1202
|
** --body FILENAME Content from FILENAME |
|
1203
|
** --smtp-trace Trace SMTP processing |
|
1204
|
** --stdout Send msg to stdout |
|
1205
|
** -S|--subject SUBJECT Message "subject:" |
|
1206
|
** |
|
1207
|
** unsubscribe EMAIL Remove a single subscriber with the given EMAIL. |
|
1208
|
*/ |
|
1209
|
void alert_cmd(void){ |
|
1210
|
const char *zCmd; |
|
1211
|
int nCmd; |
|
1212
|
db_find_and_open_repository(0, 0); |
|
1213
|
alert_schema(0); |
|
1214
|
zCmd = g.argc>=3 ? g.argv[2] : "x"; |
|
1215
|
nCmd = (int)strlen(zCmd); |
|
1216
|
if( strncmp(zCmd, "pending", nCmd)==0 ){ |
|
1217
|
Stmt q; |
|
1218
|
verify_all_options(); |
|
1219
|
if( g.argc!=3 ) usage("pending"); |
|
1220
|
db_prepare(&q,"SELECT eventid, sentSep, sentDigest, sentMod" |
|
1221
|
" FROM pending_alert"); |
|
1222
|
while( db_step(&q)==SQLITE_ROW ){ |
|
1223
|
fossil_print("%10s %7s %10s %7s\n", |
|
1224
|
db_column_text(&q,0), |
|
1225
|
db_column_int(&q,1) ? "sentSep" : "", |
|
1226
|
db_column_int(&q,2) ? "sentDigest" : "", |
|
1227
|
db_column_int(&q,3) ? "sentMod" : ""); |
|
1228
|
} |
|
1229
|
db_finalize(&q); |
|
1230
|
}else |
|
1231
|
if( strncmp(zCmd, "reset", nCmd)==0 ){ |
|
1232
|
int c; |
|
1233
|
int bForce = find_option("force","f",0)!=0; |
|
1234
|
verify_all_options(); |
|
1235
|
if( bForce ){ |
|
1236
|
c = 'y'; |
|
1237
|
}else{ |
|
1238
|
Blob yn; |
|
1239
|
fossil_print( |
|
1240
|
"This will erase all content in the repository tables, thus\n" |
|
1241
|
"deleting all subscriber information. The information will be\n" |
|
1242
|
"unrecoverable.\n"); |
|
1243
|
prompt_user("Continue? (y/N) ", &yn); |
|
1244
|
c = blob_str(&yn)[0]; |
|
1245
|
blob_reset(&yn); |
|
1246
|
} |
|
1247
|
if( c=='y' ){ |
|
1248
|
alert_drop_trigger(); |
|
1249
|
db_multi_exec( |
|
1250
|
"DROP TABLE IF EXISTS subscriber;\n" |
|
1251
|
"DROP TABLE IF EXISTS pending_alert;\n" |
|
1252
|
"DROP TABLE IF EXISTS alert_bounce;\n" |
|
1253
|
/* Legacy */ |
|
1254
|
"DROP TABLE IF EXISTS alert_pending;\n" |
|
1255
|
"DROP TABLE IF EXISTS subscription;\n" |
|
1256
|
); |
|
1257
|
alert_schema(0); |
|
1258
|
} |
|
1259
|
}else |
|
1260
|
if( strncmp(zCmd, "send", nCmd)==0 ){ |
|
1261
|
u32 eFlags = 0; |
|
1262
|
if( find_option("digest",0,0)!=0 ) eFlags |= SENDALERT_DIGEST; |
|
1263
|
if( find_option("renewal",0,0)!=0 ) eFlags |= SENDALERT_RENEWAL; |
|
1264
|
if( find_option("test",0,0)!=0 ){ |
|
1265
|
eFlags |= SENDALERT_PRESERVE|SENDALERT_STDOUT; |
|
1266
|
} |
|
1267
|
verify_all_options(); |
|
1268
|
alert_send_alerts(eFlags); |
|
1269
|
}else |
|
1270
|
if( strncmp(zCmd, "settings", nCmd)==0 ){ |
|
1271
|
int isGlobal = find_option("global",0,0)!=0; |
|
1272
|
int nSetting; |
|
1273
|
const Setting *pSetting = setting_info(&nSetting); |
|
1274
|
db_open_config(1, 0); |
|
1275
|
verify_all_options(); |
|
1276
|
if( g.argc!=3 && g.argc!=5 ) usage("setting [NAME VALUE]"); |
|
1277
|
if( g.argc==5 ){ |
|
1278
|
const char *zLabel = g.argv[3]; |
|
1279
|
if( strncmp(zLabel, "email-", 6)!=0 |
|
1280
|
|| (pSetting = db_find_setting(zLabel, 1))==0 ){ |
|
1281
|
fossil_fatal("not a valid email setting: \"%s\"", zLabel); |
|
1282
|
} |
|
1283
|
db_set(pSetting->name/*works-like:""*/, g.argv[4], isGlobal); |
|
1284
|
g.argc = 3; |
|
1285
|
} |
|
1286
|
pSetting = setting_info(&nSetting); |
|
1287
|
for(; nSetting>0; nSetting--, pSetting++ ){ |
|
1288
|
if( strncmp(pSetting->name,"email-",6)!=0 ) continue; |
|
1289
|
print_setting(pSetting, 0, 0); |
|
1290
|
} |
|
1291
|
}else |
|
1292
|
if( strncmp(zCmd, "status", nCmd)==0 ){ |
|
1293
|
Stmt q; |
|
1294
|
int iCutoff; |
|
1295
|
int nSetting, n; |
|
1296
|
static const char *zFmt = "%-29s %d\n"; |
|
1297
|
const Setting *pSetting = setting_info(&nSetting); |
|
1298
|
db_open_config(1, 0); |
|
1299
|
verify_all_options(); |
|
1300
|
if( g.argc!=3 ) usage("status"); |
|
1301
|
pSetting = setting_info(&nSetting); |
|
1302
|
for(; nSetting>0; nSetting--, pSetting++ ){ |
|
1303
|
if( strncmp(pSetting->name,"email-",6)!=0 ) continue; |
|
1304
|
print_setting(pSetting, 0, 0); |
|
1305
|
} |
|
1306
|
n = db_int(0,"SELECT count(*) FROM pending_alert WHERE NOT sentSep"); |
|
1307
|
fossil_print(zFmt/*works-like:"%s%d"*/, "pending-alerts", n); |
|
1308
|
n = db_int(0,"SELECT count(*) FROM pending_alert WHERE NOT sentDigest"); |
|
1309
|
fossil_print(zFmt/*works-like:"%s%d"*/, "pending-digest-alerts", n); |
|
1310
|
db_prepare(&q, |
|
1311
|
"SELECT" |
|
1312
|
" name," |
|
1313
|
" value," |
|
1314
|
" now()/86400-value," |
|
1315
|
" date(value*86400,'unixepoch')" |
|
1316
|
" FROM repository.config" |
|
1317
|
" WHERE name in ('email-renew-warning','email-renew-cutoff');"); |
|
1318
|
while( db_step(&q)==SQLITE_ROW ){ |
|
1319
|
fossil_print("%-29s %-6d (%d days ago on %s)\n", |
|
1320
|
db_column_text(&q, 0), |
|
1321
|
db_column_int(&q, 1), |
|
1322
|
db_column_int(&q, 2), |
|
1323
|
db_column_text(&q, 3)); |
|
1324
|
} |
|
1325
|
db_finalize(&q); |
|
1326
|
n = db_int(0,"SELECT count(*) FROM subscriber"); |
|
1327
|
fossil_print(zFmt/*works-like:"%s%d"*/, "total-subscribers", n); |
|
1328
|
iCutoff = db_get_int("email-renew-cutoff", 0); |
|
1329
|
n = db_int(0, "SELECT count(*) FROM subscriber WHERE sverified" |
|
1330
|
" AND NOT sdonotcall AND length(ssub)>1" |
|
1331
|
" AND lastContact>=%d", iCutoff); |
|
1332
|
fossil_print(zFmt/*works-like:"%s%d"*/, "active-subscribers", n); |
|
1333
|
}else |
|
1334
|
if( strncmp(zCmd, "subscribers", nCmd)==0 ){ |
|
1335
|
Stmt q; |
|
1336
|
verify_all_options(); |
|
1337
|
if( g.argc!=3 && g.argc!=4 ) usage("subscribers [PATTERN]"); |
|
1338
|
if( g.argc==4 ){ |
|
1339
|
char *zPattern = g.argv[3]; |
|
1340
|
db_prepare(&q, |
|
1341
|
"SELECT semail FROM subscriber" |
|
1342
|
" WHERE semail LIKE '%%%q%%' OR suname LIKE '%%%q%%'" |
|
1343
|
" OR semail GLOB '*%q*' or suname GLOB '*%q*'" |
|
1344
|
" ORDER BY semail", |
|
1345
|
zPattern, zPattern, zPattern, zPattern); |
|
1346
|
}else{ |
|
1347
|
db_prepare(&q, |
|
1348
|
"SELECT semail FROM subscriber" |
|
1349
|
" ORDER BY semail"); |
|
1350
|
} |
|
1351
|
while( db_step(&q)==SQLITE_ROW ){ |
|
1352
|
fossil_print("%s\n", db_column_text(&q, 0)); |
|
1353
|
} |
|
1354
|
db_finalize(&q); |
|
1355
|
}else |
|
1356
|
if( strncmp(zCmd, "test-message", nCmd)==0 ){ |
|
1357
|
Blob prompt, body, hdr; |
|
1358
|
const char *zDest = find_option("stdout",0,0)!=0 ? "stdout" : 0; |
|
1359
|
int i; |
|
1360
|
u32 mFlags = ALERT_IMMEDIATE_FAIL; |
|
1361
|
const char *zSubject = find_option("subject", "S", 1); |
|
1362
|
const char *zSource = find_option("body", 0, 1); |
|
1363
|
AlertSender *pSender; |
|
1364
|
if( find_option("smtp-trace",0,0)!=0 ) mFlags |= ALERT_TRACE; |
|
1365
|
verify_all_options(); |
|
1366
|
blob_init(&prompt, 0, 0); |
|
1367
|
blob_init(&body, 0, 0); |
|
1368
|
blob_init(&hdr, 0, 0); |
|
1369
|
blob_appendf(&hdr,"To: "); |
|
1370
|
for(i=3; i<g.argc; i++){ |
|
1371
|
if( i>3 ) blob_append(&hdr, ", ", 2); |
|
1372
|
blob_appendf(&hdr, "<%s>", g.argv[i]); |
|
1373
|
} |
|
1374
|
blob_append(&hdr,"\r\n",2); |
|
1375
|
if( zSubject==0 ) zSubject = "fossil alerts test-message"; |
|
1376
|
blob_appendf(&hdr, "Subject: %s\r\n", zSubject); |
|
1377
|
if( zSource ){ |
|
1378
|
blob_read_from_file(&body, zSource, ExtFILE); |
|
1379
|
}else{ |
|
1380
|
prompt_for_user_comment(&body, &prompt); |
|
1381
|
} |
|
1382
|
blob_add_final_newline(&body); |
|
1383
|
pSender = alert_sender_new(zDest, mFlags); |
|
1384
|
alert_send(pSender, &hdr, &body, 0); |
|
1385
|
alert_sender_free(pSender); |
|
1386
|
blob_reset(&hdr); |
|
1387
|
blob_reset(&body); |
|
1388
|
blob_reset(&prompt); |
|
1389
|
}else |
|
1390
|
if( strncmp(zCmd, "unsubscribe", nCmd)==0 ){ |
|
1391
|
verify_all_options(); |
|
1392
|
if( g.argc!=4 ) usage("unsubscribe EMAIL"); |
|
1393
|
db_multi_exec( |
|
1394
|
"DELETE FROM subscriber WHERE semail=%Q", g.argv[3]); |
|
1395
|
}else |
|
1396
|
{ |
|
1397
|
usage("pending|reset|send|setting|status|" |
|
1398
|
"subscribers|test-message|unsubscribe"); |
|
1399
|
} |
|
1400
|
} |
|
1401
|
|
|
1402
|
/* |
|
1403
|
** Do error checking on a submitted subscription form. Return TRUE |
|
1404
|
** if the submission is valid. Return false if any problems are seen. |
|
1405
|
*/ |
|
1406
|
static int subscribe_error_check( |
|
1407
|
int *peErr, /* Type of error */ |
|
1408
|
char **pzErr, /* Error message text */ |
|
1409
|
int needCaptcha /* True if captcha check needed */ |
|
1410
|
){ |
|
1411
|
const char *zEAddr; |
|
1412
|
int i, j, n; |
|
1413
|
char c; |
|
1414
|
|
|
1415
|
*peErr = 0; |
|
1416
|
*pzErr = 0; |
|
1417
|
|
|
1418
|
/* Verify the captcha first */ |
|
1419
|
if( needCaptcha ){ |
|
1420
|
if( !captcha_is_correct(1) ){ |
|
1421
|
*peErr = 2; |
|
1422
|
*pzErr = mprintf("incorrect security code"); |
|
1423
|
return 0; |
|
1424
|
} |
|
1425
|
} |
|
1426
|
|
|
1427
|
/* Check the validity of the email address. |
|
1428
|
** |
|
1429
|
** (1) Exactly one '@' character. |
|
1430
|
** (2) No other characters besides [a-zA-Z0-9._+-] |
|
1431
|
** |
|
1432
|
** The local part is currently more restrictive than RFC 5322 allows: |
|
1433
|
** https://stackoverflow.com/a/2049510/142454 We will expand this as |
|
1434
|
** necessary. |
|
1435
|
*/ |
|
1436
|
zEAddr = P("e"); |
|
1437
|
if( zEAddr==0 ){ |
|
1438
|
*peErr = 1; |
|
1439
|
*pzErr = mprintf("required"); |
|
1440
|
return 0; |
|
1441
|
} |
|
1442
|
for(i=j=n=0; (c = zEAddr[i])!=0; i++){ |
|
1443
|
if( c=='@' ){ |
|
1444
|
n = i; |
|
1445
|
j++; |
|
1446
|
continue; |
|
1447
|
} |
|
1448
|
if( !fossil_isalnum(c) && c!='.' && c!='_' && c!='-' && c!='+' ){ |
|
1449
|
*peErr = 1; |
|
1450
|
*pzErr = mprintf("illegal character in email address: 0x%x '%c'", |
|
1451
|
c, c); |
|
1452
|
return 0; |
|
1453
|
} |
|
1454
|
} |
|
1455
|
if( j!=1 ){ |
|
1456
|
*peErr = 1; |
|
1457
|
*pzErr = mprintf("email address should contain exactly one '@'"); |
|
1458
|
return 0; |
|
1459
|
} |
|
1460
|
if( n<1 ){ |
|
1461
|
*peErr = 1; |
|
1462
|
*pzErr = mprintf("name missing before '@' in email address"); |
|
1463
|
return 0; |
|
1464
|
} |
|
1465
|
if( n>i-5 ){ |
|
1466
|
*peErr = 1; |
|
1467
|
*pzErr = mprintf("email domain too short"); |
|
1468
|
return 0; |
|
1469
|
} |
|
1470
|
|
|
1471
|
if( authorized_subscription_email(zEAddr)==0 ){ |
|
1472
|
*peErr = 1; |
|
1473
|
*pzErr = mprintf("not an authorized email address"); |
|
1474
|
return 0; |
|
1475
|
} |
|
1476
|
|
|
1477
|
/* Check to make sure the email address is available for reuse */ |
|
1478
|
if( db_exists("SELECT 1 FROM subscriber WHERE semail=%Q", zEAddr) ){ |
|
1479
|
*peErr = 1; |
|
1480
|
*pzErr = mprintf("this email address is used by someone else"); |
|
1481
|
return 0; |
|
1482
|
} |
|
1483
|
|
|
1484
|
/* If we reach this point, all is well */ |
|
1485
|
return 1; |
|
1486
|
} |
|
1487
|
|
|
1488
|
/* |
|
1489
|
** Text of email message sent in order to confirm a subscription. |
|
1490
|
*/ |
|
1491
|
static const char zConfirmMsg[] = |
|
1492
|
@ Someone has signed you up for email alerts on the Fossil repository |
|
1493
|
@ at %s. |
|
1494
|
@ |
|
1495
|
@ To confirm your subscription and begin receiving alerts, click on |
|
1496
|
@ the following hyperlink: |
|
1497
|
@ |
|
1498
|
@ %s/alerts/%s |
|
1499
|
@ |
|
1500
|
@ Save the hyperlink above! You can reuse this same hyperlink to |
|
1501
|
@ unsubscribe or to change the kinds of alerts you receive. |
|
1502
|
@ |
|
1503
|
@ If you do not want to subscribe, you can simply ignore this message. |
|
1504
|
@ You will not be contacted again. |
|
1505
|
@ |
|
1506
|
; |
|
1507
|
|
|
1508
|
/* |
|
1509
|
** Append the text of an email confirmation message to the given |
|
1510
|
** Blob. The security code is in zCode. |
|
1511
|
*/ |
|
1512
|
void alert_append_confirmation_message(Blob *pMsg, const char *zCode){ |
|
1513
|
blob_appendf(pMsg, zConfirmMsg/*works-like:"%s%s%s"*/, |
|
1514
|
g.zBaseURL, g.zBaseURL, zCode); |
|
1515
|
} |
|
1516
|
|
|
1517
|
/* |
|
1518
|
** WEBPAGE: subscribe |
|
1519
|
** |
|
1520
|
** Allow users to subscribe to email notifications. |
|
1521
|
** |
|
1522
|
** This page is usually run by users who are not logged in. |
|
1523
|
** A logged-in user can add email notifications on the /alerts page. |
|
1524
|
** Access to this page by a logged in user (other than an |
|
1525
|
** administrator) results in a redirect to the /alerts page. |
|
1526
|
** |
|
1527
|
** Administrators can visit this page in order to sign up other |
|
1528
|
** users. |
|
1529
|
** |
|
1530
|
** The Alerts permission ("7") is required to access this |
|
1531
|
** page. To allow anonymous passers-by to sign up for email |
|
1532
|
** notification, set Email-Alerts on user "nobody" or "anonymous". |
|
1533
|
*/ |
|
1534
|
void subscribe_page(void){ |
|
1535
|
int needCaptcha; |
|
1536
|
unsigned int uSeed = 0; |
|
1537
|
const char *zDecoded; |
|
1538
|
char *zCaptcha = 0; |
|
1539
|
char *zErr = 0; |
|
1540
|
int eErr = 0; |
|
1541
|
int di; |
|
1542
|
|
|
1543
|
if( alert_webpages_disabled() ) return; |
|
1544
|
login_check_credentials(); |
|
1545
|
if( !g.perm.EmailAlert ){ |
|
1546
|
login_needed(g.anon.EmailAlert); |
|
1547
|
return; |
|
1548
|
} |
|
1549
|
if( login_is_individual() |
|
1550
|
&& db_exists("SELECT 1 FROM subscriber WHERE suname=%Q",g.zLogin) |
|
1551
|
){ |
|
1552
|
/* This person is already signed up for email alerts. Jump |
|
1553
|
** to the screen that lets them edit their alert preferences. |
|
1554
|
** Except, administrators can create subscriptions for others so |
|
1555
|
** do not jump for them. |
|
1556
|
*/ |
|
1557
|
if( g.perm.Admin ){ |
|
1558
|
/* Admins get a link to admin their own account, but they |
|
1559
|
** stay on this page so that they can create subscriptions |
|
1560
|
** for other people. */ |
|
1561
|
style_submenu_element("My Subscription","%R/alerts"); |
|
1562
|
}else{ |
|
1563
|
/* Everybody else jumps to the page to administer their own |
|
1564
|
** account only. */ |
|
1565
|
cgi_redirectf("%R/alerts"); |
|
1566
|
return; |
|
1567
|
} |
|
1568
|
} |
|
1569
|
if( !g.perm.Admin && !db_get_boolean("anon-subscribe",1) ){ |
|
1570
|
register_page(); |
|
1571
|
return; |
|
1572
|
} |
|
1573
|
style_set_current_feature("alerts"); |
|
1574
|
alert_submenu_common(); |
|
1575
|
needCaptcha = !login_is_individual(); |
|
1576
|
if( P("submit") |
|
1577
|
&& cgi_csrf_safe(2) |
|
1578
|
&& subscribe_error_check(&eErr,&zErr,needCaptcha) |
|
1579
|
){ |
|
1580
|
/* A validated request for a new subscription has been received. */ |
|
1581
|
char ssub[20]; |
|
1582
|
const char *zEAddr = P("e"); |
|
1583
|
const char *zCode; /* New subscriber code (in hex) */ |
|
1584
|
int nsub = 0; |
|
1585
|
const char *suname = PT("suname"); |
|
1586
|
if( suname==0 && needCaptcha==0 && !g.perm.Admin ) suname = g.zLogin; |
|
1587
|
if( suname && suname[0]==0 ) suname = 0; |
|
1588
|
if( PB("sa") ) ssub[nsub++] = 'a'; |
|
1589
|
if( g.perm.Read && PB("sc") ) ssub[nsub++] = 'c'; |
|
1590
|
if( g.perm.RdForum && PB("sf") ) ssub[nsub++] = 'f'; |
|
1591
|
if( g.perm.RdForum && PB("sn") ) ssub[nsub++] = 'n'; |
|
1592
|
if( g.perm.RdForum && PB("sr") ) ssub[nsub++] = 'r'; |
|
1593
|
if( g.perm.RdTkt && PB("st") ) ssub[nsub++] = 't'; |
|
1594
|
if( g.perm.Admin && PB("su") ) ssub[nsub++] = 'u'; |
|
1595
|
if( g.perm.RdWiki && PB("sw") ) ssub[nsub++] = 'w'; |
|
1596
|
if( g.perm.RdForum && PB("sx") ) ssub[nsub++] = 'x'; |
|
1597
|
ssub[nsub] = 0; |
|
1598
|
zCode = db_text(0, |
|
1599
|
"INSERT INTO subscriber(semail,suname," |
|
1600
|
" sverified,sdonotcall,sdigest,ssub,sctime,mtime,smip,lastContact)" |
|
1601
|
"VALUES(%Q,%Q,%d,0,%d,%Q,now(),now(),%Q,now()/86400)" |
|
1602
|
"RETURNING hex(subscriberCode);", |
|
1603
|
/* semail */ zEAddr, |
|
1604
|
/* suname */ suname, |
|
1605
|
/* sverified */ needCaptcha==0, |
|
1606
|
/* sdigest */ PB("di"), |
|
1607
|
/* ssub */ ssub, |
|
1608
|
/* smip */ g.zIpAddr |
|
1609
|
); |
|
1610
|
if( !needCaptcha ){ |
|
1611
|
/* The new subscription has been added on behalf of a logged-in user. |
|
1612
|
** No verification is required. Jump immediately to /alerts page. |
|
1613
|
*/ |
|
1614
|
if( g.perm.Admin ){ |
|
1615
|
cgi_redirectf("%R/alerts/%.32s", zCode); |
|
1616
|
}else{ |
|
1617
|
cgi_redirectf("%R/alerts"); |
|
1618
|
} |
|
1619
|
return; |
|
1620
|
}else{ |
|
1621
|
/* We need to send a verification email */ |
|
1622
|
Blob hdr, body; |
|
1623
|
AlertSender *pSender = alert_sender_new(0,0); |
|
1624
|
blob_init(&hdr,0,0); |
|
1625
|
blob_init(&body,0,0); |
|
1626
|
blob_appendf(&hdr, "To: <%s>\n", zEAddr); |
|
1627
|
blob_appendf(&hdr, "Subject: Subscription verification\n"); |
|
1628
|
alert_append_confirmation_message(&body, zCode); |
|
1629
|
alert_send(pSender, &hdr, &body, 0); |
|
1630
|
style_header("Email Alert Verification"); |
|
1631
|
if( pSender->zErr ){ |
|
1632
|
@ <h1>Internal Error</h1> |
|
1633
|
@ <p>The following internal error was encountered while trying |
|
1634
|
@ to send the confirmation email: |
|
1635
|
@ <blockquote><pre> |
|
1636
|
@ %h(pSender->zErr) |
|
1637
|
@ </pre></blockquote> |
|
1638
|
}else{ |
|
1639
|
@ <p>An email has been sent to "%h(zEAddr)". That email contains a |
|
1640
|
@ hyperlink that you must click to activate your |
|
1641
|
@ subscription.</p> |
|
1642
|
} |
|
1643
|
alert_sender_free(pSender); |
|
1644
|
style_finish_page(); |
|
1645
|
} |
|
1646
|
return; |
|
1647
|
} |
|
1648
|
style_header("Signup For Email Alerts"); |
|
1649
|
if( P("submit")==0 ){ |
|
1650
|
/* If this is the first visit to this page (if this HTTP request did not |
|
1651
|
** come from a prior Submit of the form) then default all of the |
|
1652
|
** subscription options to "on" */ |
|
1653
|
cgi_set_parameter_nocopy("sa","1",1); |
|
1654
|
if( g.perm.Read ) cgi_set_parameter_nocopy("sc","1",1); |
|
1655
|
if( g.perm.RdForum ) cgi_set_parameter_nocopy("sf","1",1); |
|
1656
|
if( g.perm.RdForum ) cgi_set_parameter_nocopy("sn","1",1); |
|
1657
|
if( g.perm.RdForum ) cgi_set_parameter_nocopy("sr","1",1); |
|
1658
|
if( g.perm.RdTkt ) cgi_set_parameter_nocopy("st","1",1); |
|
1659
|
if( g.perm.Admin ) cgi_set_parameter_nocopy("su","1",1); |
|
1660
|
if( g.perm.RdWiki ) cgi_set_parameter_nocopy("sw","1",1); |
|
1661
|
} |
|
1662
|
@ <p>To receive email notifications for changes to this |
|
1663
|
@ repository, fill out the form below and press the "Submit" button.</p> |
|
1664
|
form_begin(0, "%R/subscribe"); |
|
1665
|
@ <table class="subscribe"> |
|
1666
|
@ <tr> |
|
1667
|
@ <td class="form_label">Email Address:</td> |
|
1668
|
@ <td><input type="text" name="e" value="%h(PD("e",""))" size="30"></td> |
|
1669
|
@ <tr> |
|
1670
|
if( eErr==1 ){ |
|
1671
|
@ <tr><td><td><span class='loginError'>↑ %h(zErr)</span></td></tr> |
|
1672
|
} |
|
1673
|
@ </tr> |
|
1674
|
if( needCaptcha ){ |
|
1675
|
const char *zInit = ""; |
|
1676
|
if( P("captchaseed")!=0 && eErr!=2 ){ |
|
1677
|
uSeed = strtoul(P("captchaseed"),0,10); |
|
1678
|
zInit = P("captcha"); |
|
1679
|
}else{ |
|
1680
|
uSeed = captcha_seed(); |
|
1681
|
} |
|
1682
|
zDecoded = captcha_decode(uSeed, 0); |
|
1683
|
zCaptcha = captcha_render(zDecoded); |
|
1684
|
@ <tr> |
|
1685
|
@ <td class="form_label">Security Code:</td> |
|
1686
|
@ <td><input type="text" name="captcha" value="%h(zInit)" size="30"> |
|
1687
|
captcha_speakit_button(uSeed, "Speak the code"); |
|
1688
|
@ <input type="hidden" name="captchaseed" value="%u(uSeed)"></td> |
|
1689
|
@ </tr> |
|
1690
|
if( eErr==2 ){ |
|
1691
|
@ <tr><td><td><span class='loginError'>↑ %h(zErr)</span></td></tr> |
|
1692
|
} |
|
1693
|
@ </tr> |
|
1694
|
} |
|
1695
|
if( g.perm.Admin ){ |
|
1696
|
@ <tr> |
|
1697
|
@ <td class="form_label">User:</td> |
|
1698
|
@ <td><input type="text" name="suname" value="%h(PD("suname",g.zLogin))" \ |
|
1699
|
@ size="30"></td> |
|
1700
|
@ </tr> |
|
1701
|
if( eErr==3 ){ |
|
1702
|
@ <tr><td><td><span class='loginError'>↑ %h(zErr)</span></td></tr> |
|
1703
|
} |
|
1704
|
@ </tr> |
|
1705
|
} |
|
1706
|
@ <tr> |
|
1707
|
@ <td class="form_label">Topics:</td> |
|
1708
|
@ <td><label><input type="checkbox" name="sa" %s(PCK("sa"))> \ |
|
1709
|
@ Announcements</label><br> |
|
1710
|
if( g.perm.Read ){ |
|
1711
|
@ <label><input type="checkbox" name="sc" %s(PCK("sc"))> \ |
|
1712
|
@ Check-ins</label><br> |
|
1713
|
} |
|
1714
|
if( g.perm.RdForum ){ |
|
1715
|
@ <label><input type="checkbox" name="sf" %s(PCK("sf"))> \ |
|
1716
|
@ All Forum Posts</label><br> |
|
1717
|
@ <label><input type="checkbox" name="sn" %s(PCK("sn"))> \ |
|
1718
|
@ New Forum Threads</label><br> |
|
1719
|
@ <label><input type="checkbox" name="sr" %s(PCK("sr"))> \ |
|
1720
|
@ Replies To My Forum Posts</label><br> |
|
1721
|
@ <label><input type="checkbox" name="sx" %s(PCK("sx"))> \ |
|
1722
|
@ Edits To Forum Posts</label><br> |
|
1723
|
} |
|
1724
|
if( g.perm.RdTkt ){ |
|
1725
|
@ <label><input type="checkbox" name="st" %s(PCK("st"))> \ |
|
1726
|
@ Ticket changes</label><br> |
|
1727
|
} |
|
1728
|
if( g.perm.RdWiki ){ |
|
1729
|
@ <label><input type="checkbox" name="sw" %s(PCK("sw"))> \ |
|
1730
|
@ Wiki</label><br> |
|
1731
|
} |
|
1732
|
if( g.perm.Admin ){ |
|
1733
|
@ <label><input type="checkbox" name="su" %s(PCK("su"))> \ |
|
1734
|
@ User permission changes</label> |
|
1735
|
} |
|
1736
|
di = PB("di"); |
|
1737
|
@ </td></tr> |
|
1738
|
@ <tr> |
|
1739
|
@ <td class="form_label">Delivery:</td> |
|
1740
|
@ <td><select size="1" name="di"> |
|
1741
|
@ <option value="0" %s(di?"":"selected")>Individual Emails</option> |
|
1742
|
@ <option value="1" %s(di?"selected":"")>Daily Digest</option> |
|
1743
|
@ </select></td> |
|
1744
|
@ </tr> |
|
1745
|
if( g.perm.Admin ){ |
|
1746
|
@ <tr> |
|
1747
|
@ <td class="form_label">Admin Options:</td><td> |
|
1748
|
@ <label><input type="checkbox" name="vi" %s(PCK("vi"))> \ |
|
1749
|
@ Verified</label><br> |
|
1750
|
@ <label><input type="checkbox" name="dnc" %s(PCK("dnc"))> \ |
|
1751
|
@ Do not call</label></td></tr> |
|
1752
|
} |
|
1753
|
@ <tr> |
|
1754
|
@ <td></td> |
|
1755
|
if( needCaptcha && !alert_enabled() ){ |
|
1756
|
@ <td><input type="submit" name="submit" value="Submit" disabled> |
|
1757
|
@ (Email current disabled)</td> |
|
1758
|
}else{ |
|
1759
|
@ <td><input type="submit" name="submit" value="Submit"></td> |
|
1760
|
} |
|
1761
|
@ </tr> |
|
1762
|
@ </table> |
|
1763
|
if( needCaptcha ){ |
|
1764
|
@ <div class="captcha"><table class="captcha"><tr><td><pre class="captcha"> |
|
1765
|
@ %h(zCaptcha) |
|
1766
|
@ </pre> |
|
1767
|
@ Enter the 8 characters above in the "Security Code" box<br/> |
|
1768
|
@ </td></tr></table></div> |
|
1769
|
} |
|
1770
|
@ </form> |
|
1771
|
fossil_free(zErr); |
|
1772
|
style_finish_page(); |
|
1773
|
} |
|
1774
|
|
|
1775
|
/* |
|
1776
|
** Either shutdown or completely delete a subscription entry given |
|
1777
|
** by the hex value zName. Then paint a webpage that explains that |
|
1778
|
** the entry has been removed. |
|
1779
|
*/ |
|
1780
|
static void alert_unsubscribe(int sid, int bTotal){ |
|
1781
|
const char *zEmail = 0; |
|
1782
|
const char *zLogin = 0; |
|
1783
|
int uid = 0; |
|
1784
|
Stmt q; |
|
1785
|
db_prepare(&q, "SELECT semail, suname FROM subscriber" |
|
1786
|
" WHERE subscriberId=%d", sid); |
|
1787
|
if( db_step(&q)==SQLITE_ROW ){ |
|
1788
|
zEmail = db_column_text(&q, 0); |
|
1789
|
zLogin = db_column_text(&q, 1); |
|
1790
|
uid = db_int(0, "SELECT uid FROM user WHERE login=%Q", zLogin); |
|
1791
|
} |
|
1792
|
style_set_current_feature("alerts"); |
|
1793
|
if( zEmail==0 ){ |
|
1794
|
style_header("Unsubscribe Fail"); |
|
1795
|
@ <p>Unable to locate a subscriber with the requested key</p> |
|
1796
|
}else{ |
|
1797
|
db_unprotect(PROTECT_READONLY); |
|
1798
|
if( bTotal ){ |
|
1799
|
/* Completely delete the subscriber */ |
|
1800
|
db_multi_exec( |
|
1801
|
"DELETE FROM subscriber WHERE subscriberId=%d", sid |
|
1802
|
); |
|
1803
|
}else{ |
|
1804
|
/* Keep the subscriber, but turn off all notifications */ |
|
1805
|
db_multi_exec( |
|
1806
|
"UPDATE subscriber SET ssub='k', mtime=now() WHERE subscriberId=%d", |
|
1807
|
sid |
|
1808
|
); |
|
1809
|
} |
|
1810
|
db_protect_pop(); |
|
1811
|
style_header("Unsubscribed"); |
|
1812
|
@ <p>The "%h(zEmail)" email address has been unsubscribed from all |
|
1813
|
@ notifications. All subscription records for "%h(zEmail)" have |
|
1814
|
@ been purged. No further emails will be sent to "%h(zEmail)".</p> |
|
1815
|
if( uid && g.perm.Admin ){ |
|
1816
|
@ <p>You may also want to |
|
1817
|
@ <a href="%R/setup_uedit?id=%d(uid)">edit or delete |
|
1818
|
@ the corresponding user "%h(zLogin)"</a></p> |
|
1819
|
} |
|
1820
|
} |
|
1821
|
db_finalize(&q); |
|
1822
|
style_finish_page(); |
|
1823
|
return; |
|
1824
|
} |
|
1825
|
|
|
1826
|
/* |
|
1827
|
** WEBPAGE: alerts |
|
1828
|
** |
|
1829
|
** Edit email alert and notification settings. |
|
1830
|
** |
|
1831
|
** The subscriber is identified in several ways: |
|
1832
|
** |
|
1833
|
** * The name= query parameter contains the complete subscriberCode. |
|
1834
|
** This only happens when the user receives a verification |
|
1835
|
** email and clicks on the link in the email. When a |
|
1836
|
** compilete subscriberCode is seen on the name= query parameter, |
|
1837
|
** that constitutes verification of the email address. |
|
1838
|
** |
|
1839
|
** * The sid= query parameter contains an integer subscriberId. |
|
1840
|
** This only works for the administrator. It allows the |
|
1841
|
** administrator to edit any subscription. |
|
1842
|
** |
|
1843
|
** * The user is logged into an account other than "nobody" or |
|
1844
|
** "anonymous". In that case the notification settings |
|
1845
|
** associated with that account can be edited without needing |
|
1846
|
** to know the subscriber code. |
|
1847
|
** |
|
1848
|
** * The name= query parameter contains a 32-digit prefix of |
|
1849
|
** subscriber code. (Subscriber codes are normally 64 hex digits |
|
1850
|
** in length.) This uniquely identifies the subscriber without |
|
1851
|
** revealing the complete subscriber code, and hence without |
|
1852
|
** verifying the email address. |
|
1853
|
*/ |
|
1854
|
void alert_page(void){ |
|
1855
|
const char *zName = 0; /* Value of the name= query parameter */ |
|
1856
|
Stmt q; /* For querying the database */ |
|
1857
|
int sa, sc, sf, st, su, sw, sx; /* Types of notifications requested */ |
|
1858
|
int sn, sr; |
|
1859
|
int sdigest = 0, sdonotcall = 0, sverified = 0; /* Other fields */ |
|
1860
|
int isLogin; /* True if logged in as an individual */ |
|
1861
|
const char *ssub = 0; /* Subscription flags */ |
|
1862
|
const char *semail = 0; /* Email address */ |
|
1863
|
const char *smip; /* */ |
|
1864
|
const char *suname = 0; /* Corresponding user.login value */ |
|
1865
|
const char *mtime; /* */ |
|
1866
|
const char *sctime; /* Time subscription created */ |
|
1867
|
int eErr = 0; /* Type of error */ |
|
1868
|
char *zErr = 0; /* Error message text */ |
|
1869
|
int sid = 0; /* Subscriber ID */ |
|
1870
|
int nName; /* Length of zName in bytes */ |
|
1871
|
char *zHalfCode; /* prefix of subscriberCode */ |
|
1872
|
int keepAlive = 0; /* True to update the last contact time */ |
|
1873
|
|
|
1874
|
db_begin_transaction(); |
|
1875
|
if( alert_webpages_disabled() ){ |
|
1876
|
db_commit_transaction(); |
|
1877
|
return; |
|
1878
|
} |
|
1879
|
login_check_credentials(); |
|
1880
|
isLogin = login_is_individual(); |
|
1881
|
zName = P("name"); |
|
1882
|
nName = zName ? (int)strlen(zName) : 0; |
|
1883
|
if( g.perm.Admin && P("sid")!=0 ){ |
|
1884
|
sid = atoi(P("sid")); |
|
1885
|
} |
|
1886
|
if( sid==0 && nName>=32 ){ |
|
1887
|
sid = db_int(0, |
|
1888
|
"SELECT CASE WHEN hex(subscriberCode) LIKE (%Q||'%%')" |
|
1889
|
" THEN subscriberId ELSE 0 END" |
|
1890
|
" FROM subscriber WHERE subscriberCode>=hextoblob(%Q)" |
|
1891
|
" LIMIT 1", zName, zName); |
|
1892
|
if( sid ) keepAlive = 1; |
|
1893
|
} |
|
1894
|
if( sid==0 && isLogin && g.perm.EmailAlert ){ |
|
1895
|
sid = db_int(0, "SELECT subscriberId FROM subscriber" |
|
1896
|
" WHERE suname=%Q", g.zLogin); |
|
1897
|
} |
|
1898
|
if( sid==0 ){ |
|
1899
|
db_commit_transaction(); |
|
1900
|
cgi_redirect("subscribe"); |
|
1901
|
/*NOTREACHED*/ |
|
1902
|
} |
|
1903
|
alert_submenu_common(); |
|
1904
|
if( P("submit")!=0 && cgi_csrf_safe(2) ){ |
|
1905
|
char newSsub[10]; |
|
1906
|
int nsub = 0; |
|
1907
|
Blob update; |
|
1908
|
|
|
1909
|
sdonotcall = PB("sdonotcall"); |
|
1910
|
sdigest = PB("sdigest"); |
|
1911
|
semail = P("semail"); |
|
1912
|
if( PB("sa") ) newSsub[nsub++] = 'a'; |
|
1913
|
if( g.perm.Read && PB("sc") ) newSsub[nsub++] = 'c'; |
|
1914
|
if( g.perm.RdForum && PB("sf") ) newSsub[nsub++] = 'f'; |
|
1915
|
if( g.perm.RdForum && PB("sn") ) newSsub[nsub++] = 'n'; |
|
1916
|
if( g.perm.RdForum && PB("sr") ) newSsub[nsub++] = 'r'; |
|
1917
|
if( g.perm.RdTkt && PB("st") ) newSsub[nsub++] = 't'; |
|
1918
|
if( g.perm.Admin && PB("su") ) newSsub[nsub++] = 'u'; |
|
1919
|
if( g.perm.RdWiki && PB("sw") ) newSsub[nsub++] = 'w'; |
|
1920
|
if( g.perm.RdForum && PB("sx") ) newSsub[nsub++] = 'x'; |
|
1921
|
newSsub[nsub] = 0; |
|
1922
|
ssub = newSsub; |
|
1923
|
blob_init(&update, "UPDATE subscriber SET", -1); |
|
1924
|
blob_append_sql(&update, |
|
1925
|
" sdonotcall=%d," |
|
1926
|
" sdigest=%d," |
|
1927
|
" ssub=%Q," |
|
1928
|
" mtime=now()," |
|
1929
|
" lastContact=now()/86400," |
|
1930
|
" smip=%Q", |
|
1931
|
sdonotcall, |
|
1932
|
sdigest, |
|
1933
|
ssub, |
|
1934
|
g.zIpAddr |
|
1935
|
); |
|
1936
|
if( g.perm.Admin ){ |
|
1937
|
suname = PT("suname"); |
|
1938
|
sverified = PB("sverified"); |
|
1939
|
if( suname && suname[0]==0 ) suname = 0; |
|
1940
|
blob_append_sql(&update, |
|
1941
|
", suname=%Q," |
|
1942
|
" sverified=%d", |
|
1943
|
suname, |
|
1944
|
sverified |
|
1945
|
); |
|
1946
|
} |
|
1947
|
if( isLogin ){ |
|
1948
|
if( semail==0 || email_address_is_valid(semail,0)==0 ){ |
|
1949
|
eErr = 8; |
|
1950
|
} |
|
1951
|
blob_append_sql(&update, ", semail=%Q", semail); |
|
1952
|
} |
|
1953
|
blob_append_sql(&update," WHERE subscriberId=%d", sid); |
|
1954
|
if( eErr==0 ){ |
|
1955
|
db_exec_sql(blob_str(&update)); |
|
1956
|
ssub = 0; |
|
1957
|
} |
|
1958
|
blob_reset(&update); |
|
1959
|
}else if( keepAlive ){ |
|
1960
|
db_unprotect(PROTECT_READONLY); |
|
1961
|
db_multi_exec( |
|
1962
|
"UPDATE subscriber SET lastContact=now()/86400" |
|
1963
|
" WHERE subscriberId=%d", sid |
|
1964
|
); |
|
1965
|
db_protect_pop(); |
|
1966
|
} |
|
1967
|
if( P("delete")!=0 && cgi_csrf_safe(2) ){ |
|
1968
|
if( !PB("dodelete") ){ |
|
1969
|
eErr = 9; |
|
1970
|
zErr = mprintf("Select this checkbox and press \"Unsubscribe\" again to" |
|
1971
|
" unsubscribe"); |
|
1972
|
}else{ |
|
1973
|
alert_unsubscribe(sid, 1); |
|
1974
|
db_commit_transaction(); |
|
1975
|
return; |
|
1976
|
} |
|
1977
|
} |
|
1978
|
style_set_current_feature("alerts"); |
|
1979
|
style_header("Update Subscription"); |
|
1980
|
db_prepare(&q, |
|
1981
|
"SELECT" |
|
1982
|
" semail," /* 0 */ |
|
1983
|
" sverified," /* 1 */ |
|
1984
|
" sdonotcall," /* 2 */ |
|
1985
|
" sdigest," /* 3 */ |
|
1986
|
" ssub," /* 4 */ |
|
1987
|
" smip," /* 5 */ |
|
1988
|
" suname," /* 6 */ |
|
1989
|
" datetime(mtime,'unixepoch')," /* 7 */ |
|
1990
|
" datetime(sctime,'unixepoch')," /* 8 */ |
|
1991
|
" hex(subscriberCode)," /* 9 */ |
|
1992
|
" date(coalesce(lastContact*86400,mtime),'unixepoch')," /* 10 */ |
|
1993
|
" now()/86400 - coalesce(lastContact,mtime/86400)" /* 11 */ |
|
1994
|
" FROM subscriber WHERE subscriberId=%d", sid); |
|
1995
|
if( db_step(&q)!=SQLITE_ROW ){ |
|
1996
|
db_finalize(&q); |
|
1997
|
db_commit_transaction(); |
|
1998
|
cgi_redirect("subscribe"); |
|
1999
|
/*NOTREACHED*/ |
|
2000
|
} |
|
2001
|
if( ssub==0 ){ |
|
2002
|
semail = db_column_text(&q, 0); |
|
2003
|
sdonotcall = db_column_int(&q, 2); |
|
2004
|
sdigest = db_column_int(&q, 3); |
|
2005
|
ssub = db_column_text(&q, 4); |
|
2006
|
} |
|
2007
|
if( suname==0 ){ |
|
2008
|
suname = db_column_text(&q, 6); |
|
2009
|
sverified = db_column_int(&q, 1); |
|
2010
|
} |
|
2011
|
sa = strchr(ssub,'a')!=0; |
|
2012
|
sc = strchr(ssub,'c')!=0; |
|
2013
|
sf = strchr(ssub,'f')!=0; |
|
2014
|
sn = strchr(ssub,'n')!=0; |
|
2015
|
sr = strchr(ssub,'r')!=0; |
|
2016
|
st = strchr(ssub,'t')!=0; |
|
2017
|
su = strchr(ssub,'u')!=0; |
|
2018
|
sw = strchr(ssub,'w')!=0; |
|
2019
|
sx = strchr(ssub,'x')!=0; |
|
2020
|
smip = db_column_text(&q, 5); |
|
2021
|
mtime = db_column_text(&q, 7); |
|
2022
|
sctime = db_column_text(&q, 8); |
|
2023
|
if( !g.perm.Admin && !sverified ){ |
|
2024
|
if( nName==64 ){ |
|
2025
|
db_unprotect(PROTECT_READONLY); |
|
2026
|
db_multi_exec( |
|
2027
|
"UPDATE subscriber SET sverified=1" |
|
2028
|
" WHERE subscriberCode=hextoblob(%Q)", |
|
2029
|
zName); |
|
2030
|
db_protect_pop(); |
|
2031
|
if( db_get_boolean("selfreg-verify",0) ){ |
|
2032
|
char *zNewCap = db_get("default-perms","u"); |
|
2033
|
db_unprotect(PROTECT_USER); |
|
2034
|
db_multi_exec( |
|
2035
|
"UPDATE user" |
|
2036
|
" SET cap=%Q" |
|
2037
|
" WHERE cap='7' AND login=(" |
|
2038
|
" SELECT suname FROM subscriber" |
|
2039
|
" WHERE subscriberCode=hextoblob(%Q))", |
|
2040
|
zNewCap, zName |
|
2041
|
); |
|
2042
|
db_protect_pop(); |
|
2043
|
login_set_capabilities(zNewCap, 0); |
|
2044
|
} |
|
2045
|
@ <h1>Your email alert subscription has been verified!</h1> |
|
2046
|
@ <p>Use the form below to update your subscription information.</p> |
|
2047
|
@ <p>Hint: Bookmark this page so that you can more easily update |
|
2048
|
@ your subscription information in the future</p> |
|
2049
|
}else{ |
|
2050
|
@ <h2>Your email address is unverified</h2> |
|
2051
|
@ <p>You should have received an email message containing a link |
|
2052
|
@ that you must visit to verify your account. No email notifications |
|
2053
|
@ will be sent until your email address has been verified.</p> |
|
2054
|
} |
|
2055
|
}else{ |
|
2056
|
@ <p>Make changes to the email subscription shown below and |
|
2057
|
@ press "Submit".</p> |
|
2058
|
} |
|
2059
|
form_begin(0, "%R/alerts"); |
|
2060
|
zHalfCode = db_text("x","SELECT hex(substr(subscriberCode,1,16))" |
|
2061
|
" FROM subscriber WHERE subscriberId=%d", sid); |
|
2062
|
@ <input type="hidden" name="name" value="%h(zHalfCode)"> |
|
2063
|
@ <table class="subscribe"> |
|
2064
|
@ <tr> |
|
2065
|
@ <td class="form_label">Email Address:</td> |
|
2066
|
if( isLogin ){ |
|
2067
|
@ <td><input type="text" name="semail" value="%h(semail)" size="30">\ |
|
2068
|
if( eErr==8 ){ |
|
2069
|
@ <span class='loginError'>← not a valid email address!</span> |
|
2070
|
}else if( g.perm.Admin ){ |
|
2071
|
@ <a href="%R/announce?to=%t(semail)">\ |
|
2072
|
@ (Send a message to %h(semail))</a>\ |
|
2073
|
} |
|
2074
|
@ </td> |
|
2075
|
}else{ |
|
2076
|
@ <td>%h(semail)</td> |
|
2077
|
} |
|
2078
|
@ </tr> |
|
2079
|
if( g.perm.Admin ){ |
|
2080
|
int uid; |
|
2081
|
@ <tr> |
|
2082
|
@ <td class='form_label'>Created:</td> |
|
2083
|
@ <td>%h(sctime)</td> |
|
2084
|
@ </tr> |
|
2085
|
@ <tr> |
|
2086
|
@ <td class='form_label'>Last Modified:</td> |
|
2087
|
@ <td>%h(mtime)</td> |
|
2088
|
@ </tr> |
|
2089
|
@ <tr> |
|
2090
|
@ <td class='form_label'>IP Address:</td> |
|
2091
|
@ <td>%h(smip)</td> |
|
2092
|
@ </tr> |
|
2093
|
@ <tr> |
|
2094
|
@ <td class='form_label'>Subscriber Code:</td> |
|
2095
|
@ <td>%h(db_column_text(&q,9))</td> |
|
2096
|
@ <tr> |
|
2097
|
@ <tr> |
|
2098
|
@ <td class='form_label'>Last Contact:</td> |
|
2099
|
@ <td>%h(db_column_text(&q,10)) ← \ |
|
2100
|
@ %,d(db_column_int(&q,11)) days ago</td> |
|
2101
|
@ </tr> |
|
2102
|
@ <td class="form_label">User:</td> |
|
2103
|
@ <td><input type="text" name="suname" value="%h(suname?suname:"")" \ |
|
2104
|
@ size="30">\ |
|
2105
|
uid = db_int(0, "SELECT uid FROM user WHERE login=%Q", suname); |
|
2106
|
if( uid ){ |
|
2107
|
@ <a href='%R/setup_uedit?id=%d(uid)'>\ |
|
2108
|
@ (login info for %h(suname))</a>\ |
|
2109
|
} |
|
2110
|
@ </tr> |
|
2111
|
} |
|
2112
|
@ <tr> |
|
2113
|
@ <td class="form_label">Topics:</td> |
|
2114
|
@ <td><label><input type="checkbox" name="sa" %s(sa?"checked":"")>\ |
|
2115
|
@ Announcements</label><br> |
|
2116
|
if( g.perm.Read ){ |
|
2117
|
@ <label><input type="checkbox" name="sc" %s(sc?"checked":"")>\ |
|
2118
|
@ Check-ins</label><br> |
|
2119
|
} |
|
2120
|
if( g.perm.RdForum ){ |
|
2121
|
@ <label><input type="checkbox" name="sf" %s(sf?"checked":"")>\ |
|
2122
|
@ All Forum Posts</label><br> |
|
2123
|
@ <label><input type="checkbox" name="sn" %s(sn?"checked":"")>\ |
|
2124
|
@ New Forum Threads</label><br> |
|
2125
|
@ <label><input type="checkbox" name="sr" %s(sr?"checked":"")>\ |
|
2126
|
@ Replies To My Posts</label><br> |
|
2127
|
@ <label><input type="checkbox" name="sx" %s(sx?"checked":"")>\ |
|
2128
|
@ Edits To Forum Posts</label><br> |
|
2129
|
} |
|
2130
|
if( g.perm.RdTkt ){ |
|
2131
|
@ <label><input type="checkbox" name="st" %s(st?"checked":"")>\ |
|
2132
|
@ Ticket changes</label><br> |
|
2133
|
} |
|
2134
|
if( g.perm.RdWiki ){ |
|
2135
|
@ <label><input type="checkbox" name="sw" %s(sw?"checked":"")>\ |
|
2136
|
@ Wiki</label><br> |
|
2137
|
} |
|
2138
|
if( g.perm.Admin ){ |
|
2139
|
/* Corner-case bug: if an admin assigns 'u' to a non-admin, that |
|
2140
|
** subscription will get removed if the user later edits their |
|
2141
|
** subscriptions, as non-admins are not permitted to add that |
|
2142
|
** subscription. */ |
|
2143
|
@ <label><input type="checkbox" name="su" %s(su?"checked":"")>\ |
|
2144
|
@ User permission changes</label> |
|
2145
|
} |
|
2146
|
@ </td></tr> |
|
2147
|
if( strchr(ssub,'k')!=0 ){ |
|
2148
|
@ <tr><td></td><td> ↑ |
|
2149
|
@ Note: User did a one-click unsubscribe</td></tr> |
|
2150
|
} |
|
2151
|
@ <tr> |
|
2152
|
@ <td class="form_label">Delivery:</td> |
|
2153
|
@ <td><select size="1" name="sdigest"> |
|
2154
|
@ <option value="0" %s(sdigest?"":"selected")>Individual Emails</option> |
|
2155
|
@ <option value="1" %s(sdigest?"selected":"")>Daily Digest</option> |
|
2156
|
@ </select></td> |
|
2157
|
@ </tr> |
|
2158
|
if( g.perm.Admin ){ |
|
2159
|
@ <tr> |
|
2160
|
@ <td class="form_label">Admin Options:</td><td> |
|
2161
|
@ <label><input type="checkbox" name="sdonotcall" \ |
|
2162
|
@ %s(sdonotcall?"checked":"")> Do not disturb</label><br> |
|
2163
|
@ <label><input type="checkbox" name="sverified" \ |
|
2164
|
@ %s(sverified?"checked":"")>\ |
|
2165
|
@ Verified</label></td></tr> |
|
2166
|
} |
|
2167
|
if( eErr==9 ){ |
|
2168
|
@ <tr> |
|
2169
|
@ <td class="form_label">Verify:</td><td> |
|
2170
|
@ <label><input type="checkbox" name="dodelete"> |
|
2171
|
@ Unsubscribe</label> |
|
2172
|
@ <span class="loginError">← %h(zErr)</span> |
|
2173
|
@ </td></tr> |
|
2174
|
} |
|
2175
|
@ <tr> |
|
2176
|
@ <td></td> |
|
2177
|
@ <td><input type="submit" name="submit" value="Submit"> |
|
2178
|
@ <input type="submit" name="delete" value="Unsubscribe"> |
|
2179
|
@ </tr> |
|
2180
|
@ </table> |
|
2181
|
@ </form> |
|
2182
|
fossil_free(zErr); |
|
2183
|
db_finalize(&q); |
|
2184
|
style_finish_page(); |
|
2185
|
db_commit_transaction(); |
|
2186
|
return; |
|
2187
|
} |
|
2188
|
|
|
2189
|
/* |
|
2190
|
** WEBPAGE: renew |
|
2191
|
** |
|
2192
|
** Users visit this page to update the last-contact date on their |
|
2193
|
** subscription. The last-contact date is the day that the subscriber |
|
2194
|
** last interacted with the repository. If the name= query parameter |
|
2195
|
** (or POST parameter) contains a valid subscriber code, then the last-contact |
|
2196
|
** subscription associated with that subscriber code is updated to be the |
|
2197
|
** current date. |
|
2198
|
*/ |
|
2199
|
void renewal_page(void){ |
|
2200
|
const char *zName = P("name"); |
|
2201
|
int iInterval = db_get_int("email-renew-interval", 0); |
|
2202
|
Stmt s; |
|
2203
|
int rc; |
|
2204
|
|
|
2205
|
style_header("Subscription Renewal"); |
|
2206
|
if( zName==0 || strlen(zName)<4 ){ |
|
2207
|
@ <p>No subscription specified</p> |
|
2208
|
style_finish_page(); |
|
2209
|
return; |
|
2210
|
} |
|
2211
|
|
|
2212
|
if( !db_table_has_column("repository","subscriber","lastContact") |
|
2213
|
|| iInterval<1 |
|
2214
|
){ |
|
2215
|
@ <p>This repository does not expire email notification subscriptions. |
|
2216
|
@ No renewals are necessary.</p> |
|
2217
|
style_finish_page(); |
|
2218
|
return; |
|
2219
|
} |
|
2220
|
|
|
2221
|
db_unprotect(PROTECT_READONLY); |
|
2222
|
db_prepare(&s, |
|
2223
|
"UPDATE subscriber" |
|
2224
|
" SET lastContact=now()/86400" |
|
2225
|
" WHERE subscriberCode=hextoblob(%Q)" |
|
2226
|
" RETURNING semail, date('now','+%d days');", |
|
2227
|
zName, iInterval+1 |
|
2228
|
); |
|
2229
|
rc = db_step(&s); |
|
2230
|
if( rc==SQLITE_ROW ){ |
|
2231
|
@ <p>The email notification subscription for %h(db_column_text(&s,0)) |
|
2232
|
@ has been extended until %h(db_column_text(&s,1)) UTC. |
|
2233
|
}else{ |
|
2234
|
@ <p>No such subscriber-id: %h(zName)</p> |
|
2235
|
} |
|
2236
|
db_finalize(&s); |
|
2237
|
db_protect_pop(); |
|
2238
|
style_finish_page(); |
|
2239
|
} |
|
2240
|
|
|
2241
|
|
|
2242
|
/* This is the message that gets sent to describe how to change |
|
2243
|
** or modify a subscription |
|
2244
|
*/ |
|
2245
|
static const char zUnsubMsg[] = |
|
2246
|
@ To changes your subscription settings at %s visit this link: |
|
2247
|
@ |
|
2248
|
@ %s/alerts/%s |
|
2249
|
@ |
|
2250
|
@ To completely unsubscribe from %s, visit the following link: |
|
2251
|
@ |
|
2252
|
@ %s/unsubscribe/%s |
|
2253
|
; |
|
2254
|
|
|
2255
|
/* |
|
2256
|
** WEBPAGE: unsubscribe |
|
2257
|
** WEBPAGE: oneclickunsub |
|
2258
|
** |
|
2259
|
** Users visit this page to be delisted from email alerts. |
|
2260
|
** |
|
2261
|
** If a valid subscriber code is supplied in the name= query parameter, |
|
2262
|
** then that subscriber is delisted. |
|
2263
|
** |
|
2264
|
** Otherwise, if the users are logged in, then they are redirected |
|
2265
|
** to the /alerts page where they have an unsubscribe button. |
|
2266
|
** |
|
2267
|
** Non-logged-in users with no name= query parameter are invited to enter |
|
2268
|
** an email address to which will be sent the unsubscribe link that |
|
2269
|
** contains the correct subscriber code. |
|
2270
|
** |
|
2271
|
** The /unsubscribe page requires comfirmation. The /oneclickunsub |
|
2272
|
** page unsubscribes immediately without any need to confirm. |
|
2273
|
*/ |
|
2274
|
void unsubscribe_page(void){ |
|
2275
|
const char *zName = P("name"); |
|
2276
|
char *zErr = 0; |
|
2277
|
int eErr = 0; |
|
2278
|
unsigned int uSeed = 0; |
|
2279
|
const char *zDecoded; |
|
2280
|
char *zCaptcha = 0; |
|
2281
|
int dx; |
|
2282
|
int bSubmit; |
|
2283
|
const char *zEAddr; |
|
2284
|
char *zCode = 0; |
|
2285
|
int sid = 0; |
|
2286
|
|
|
2287
|
if( zName==0 ) zName = P("scode"); |
|
2288
|
|
|
2289
|
/* If a valid subscriber code is supplied, then either present the user |
|
2290
|
** with a confirmation, or if already confirmed, unsubscribe immediately. |
|
2291
|
*/ |
|
2292
|
if( zName |
|
2293
|
&& (sid = db_int(0, "SELECT subscriberId FROM subscriber" |
|
2294
|
" WHERE subscriberCode=hextoblob(%Q)", zName))!=0 |
|
2295
|
){ |
|
2296
|
char *zUnsubName = mprintf("confirm%04x", sid); |
|
2297
|
if( P(zUnsubName)!=0 ){ |
|
2298
|
alert_unsubscribe(sid, 1); |
|
2299
|
}else if( sqlite3_strglob("*oneclick*",g.zPath)==0 ){ |
|
2300
|
alert_unsubscribe(sid, 0); |
|
2301
|
}else if( P("manage")!=0 ){ |
|
2302
|
cgi_redirectf("%R/alerts/%s", zName); |
|
2303
|
}else{ |
|
2304
|
style_header("Unsubscribe"); |
|
2305
|
form_begin(0, "%R/unsubscribe"); |
|
2306
|
@ <input type="hidden" name="scode" value="%h(zName)"> |
|
2307
|
@ <table border="0" cellpadding="10" width="100%%"> |
|
2308
|
@ <tr><td align="right"> |
|
2309
|
@ <input type="submit" name="%h(zUnsubName)" value="Unsubscribe"> |
|
2310
|
@ </td><td><big><b>←</b></big></td> |
|
2311
|
@ <td>Cancel your subscription to %h(g.zBaseURL) notifications |
|
2312
|
@ </td><tr> |
|
2313
|
@ <tr><td align="right"> |
|
2314
|
@ <input type="submit" name="manage" \ |
|
2315
|
@ value="Manage Subscription Settings"> |
|
2316
|
@ </td><td><big><b>←</b></big></td> |
|
2317
|
@ <td>Make other changes to your subscription preferences |
|
2318
|
@ </td><tr> |
|
2319
|
@ </table> |
|
2320
|
@ </form> |
|
2321
|
style_finish_page(); |
|
2322
|
} |
|
2323
|
return; |
|
2324
|
} |
|
2325
|
|
|
2326
|
/* Logged in users are redirected to the /alerts page */ |
|
2327
|
login_check_credentials(); |
|
2328
|
if( login_is_individual() ){ |
|
2329
|
cgi_redirectf("%R/alerts"); |
|
2330
|
return; |
|
2331
|
} |
|
2332
|
|
|
2333
|
style_set_current_feature("alerts"); |
|
2334
|
|
|
2335
|
zEAddr = PD("e",""); |
|
2336
|
dx = atoi(PD("dx","0")); |
|
2337
|
bSubmit = P("submit")!=0 && P("e")!=0 && cgi_csrf_safe(2); |
|
2338
|
if( bSubmit ){ |
|
2339
|
if( !captcha_is_correct(1) ){ |
|
2340
|
eErr = 2; |
|
2341
|
zErr = mprintf("enter the security code shown below"); |
|
2342
|
bSubmit = 0; |
|
2343
|
} |
|
2344
|
} |
|
2345
|
if( bSubmit ){ |
|
2346
|
zCode = db_text(0,"SELECT hex(subscriberCode) FROM subscriber" |
|
2347
|
" WHERE semail=%Q", zEAddr); |
|
2348
|
if( zCode==0 ){ |
|
2349
|
eErr = 1; |
|
2350
|
zErr = mprintf("not a valid email address"); |
|
2351
|
bSubmit = 0; |
|
2352
|
} |
|
2353
|
} |
|
2354
|
if( bSubmit ){ |
|
2355
|
/* If we get this far, it means that a valid unsubscribe request has |
|
2356
|
** been submitted. Send the appropriate email. */ |
|
2357
|
Blob hdr, body; |
|
2358
|
AlertSender *pSender = alert_sender_new(0,0); |
|
2359
|
blob_init(&hdr,0,0); |
|
2360
|
blob_init(&body,0,0); |
|
2361
|
blob_appendf(&hdr, "To: <%s>\r\n", zEAddr); |
|
2362
|
blob_appendf(&hdr, "Subject: Unsubscribe Instructions\r\n"); |
|
2363
|
blob_appendf(&body, zUnsubMsg/*works-like:"%s%s%s%s%s%s"*/, |
|
2364
|
g.zBaseURL, g.zBaseURL, zCode, g.zBaseURL, g.zBaseURL, zCode); |
|
2365
|
alert_send(pSender, &hdr, &body, 0); |
|
2366
|
style_header("Unsubscribe Instructions Sent"); |
|
2367
|
if( pSender->zErr ){ |
|
2368
|
@ <h1>Internal Error</h1> |
|
2369
|
@ <p>The following error was encountered while trying to send an |
|
2370
|
@ email to %h(zEAddr): |
|
2371
|
@ <blockquote><pre> |
|
2372
|
@ %h(pSender->zErr) |
|
2373
|
@ </pre></blockquote> |
|
2374
|
}else{ |
|
2375
|
@ <p>An email has been sent to "%h(zEAddr)" that explains how to |
|
2376
|
@ unsubscribe and/or modify your subscription settings</p> |
|
2377
|
} |
|
2378
|
alert_sender_free(pSender); |
|
2379
|
style_finish_page(); |
|
2380
|
return; |
|
2381
|
} |
|
2382
|
|
|
2383
|
/* Non-logged-in users have to enter an email address to which is |
|
2384
|
** sent a message containing the unsubscribe link. |
|
2385
|
*/ |
|
2386
|
style_header("Unsubscribe Request"); |
|
2387
|
@ <p>Fill out the form below to request an email message that will |
|
2388
|
@ explain how to unsubscribe and/or change your subscription settings.</p> |
|
2389
|
@ |
|
2390
|
form_begin(0, "%R/unsubscribe"); |
|
2391
|
@ <table class="subscribe"> |
|
2392
|
@ <tr> |
|
2393
|
@ <td class="form_label">Email Address:</td> |
|
2394
|
@ <td><input type="text" name="e" value="%h(zEAddr)" size="30"></td> |
|
2395
|
if( eErr==1 ){ |
|
2396
|
@ <td><span class="loginError">← %h(zErr)</span></td> |
|
2397
|
} |
|
2398
|
@ </tr> |
|
2399
|
uSeed = captcha_seed(); |
|
2400
|
zDecoded = captcha_decode(uSeed, 0); |
|
2401
|
zCaptcha = captcha_render(zDecoded); |
|
2402
|
@ <tr> |
|
2403
|
@ <td class="form_label">Security Code:</td> |
|
2404
|
@ <td><input type="text" name="captcha" value="" size="30"> |
|
2405
|
captcha_speakit_button(uSeed, "Speak the code"); |
|
2406
|
@ <input type="hidden" name="captchaseed" value="%u(uSeed)"></td> |
|
2407
|
if( eErr==2 ){ |
|
2408
|
@ <td><span class="loginError">← %h(zErr)</span></td> |
|
2409
|
} |
|
2410
|
@ </tr> |
|
2411
|
@ <tr> |
|
2412
|
@ <td class="form_label">Options:</td> |
|
2413
|
@ <td><label><input type="radio" name="dx" value="0" %s(dx?"":"checked")>\ |
|
2414
|
@ Modify subscription</label><br> |
|
2415
|
@ <label><input type="radio" name="dx" value="1" %s(dx?"checked":"")>\ |
|
2416
|
@ Completely unsubscribe</label><br> |
|
2417
|
@ <tr> |
|
2418
|
@ <td></td> |
|
2419
|
@ <td><input type="submit" name="submit" value="Submit"></td> |
|
2420
|
@ </tr> |
|
2421
|
@ </table> |
|
2422
|
@ <div class="captcha"><table class="captcha"><tr><td><pre class="captcha"> |
|
2423
|
@ %h(zCaptcha) |
|
2424
|
@ </pre> |
|
2425
|
@ Enter the 8 characters above in the "Security Code" box<br/> |
|
2426
|
@ </td></tr></table></div> |
|
2427
|
@ </form> |
|
2428
|
fossil_free(zErr); |
|
2429
|
style_finish_page(); |
|
2430
|
} |
|
2431
|
|
|
2432
|
/* |
|
2433
|
** WEBPAGE: subscribers |
|
2434
|
** |
|
2435
|
** This page, accessible to administrators only, |
|
2436
|
** shows a list of subscriber email addresses. |
|
2437
|
** Clicking on an email takes one to the /alerts page |
|
2438
|
** for that email where the delivery settings can be |
|
2439
|
** modified. |
|
2440
|
*/ |
|
2441
|
void subscriber_list_page(void){ |
|
2442
|
Blob sql; |
|
2443
|
Stmt q; |
|
2444
|
sqlite3_int64 iNow; |
|
2445
|
int nTotal; |
|
2446
|
int nPending; |
|
2447
|
int nDel = 0; |
|
2448
|
int iCutoff = db_get_int("email-renew-cutoff",0); |
|
2449
|
int iWarning = db_get_int("email-renew-warning",0); |
|
2450
|
char zCutoffClr[8]; |
|
2451
|
char zWarnClr[8]; |
|
2452
|
if( alert_webpages_disabled() ) return; |
|
2453
|
login_check_credentials(); |
|
2454
|
if( !g.perm.Admin ){ |
|
2455
|
login_needed(0); |
|
2456
|
return; |
|
2457
|
} |
|
2458
|
alert_submenu_common(); |
|
2459
|
style_submenu_element("Users","setup_ulist"); |
|
2460
|
style_set_current_feature("alerts"); |
|
2461
|
style_header("Subscriber List"); |
|
2462
|
nTotal = db_int(0, "SELECT count(*) FROM subscriber"); |
|
2463
|
nPending = db_int(0, "SELECT count(*) FROM subscriber WHERE NOT sverified"); |
|
2464
|
if( nPending>0 && P("purge") && cgi_csrf_safe(0) ){ |
|
2465
|
int nNewPending; |
|
2466
|
db_multi_exec( |
|
2467
|
"DELETE FROM subscriber" |
|
2468
|
" WHERE NOT sverified AND mtime<now()-86400" |
|
2469
|
); |
|
2470
|
nNewPending = db_int(0, "SELECT count(*) FROM subscriber" |
|
2471
|
" WHERE NOT sverified"); |
|
2472
|
nDel = nPending - nNewPending; |
|
2473
|
nPending = nNewPending; |
|
2474
|
nTotal -= nDel; |
|
2475
|
} |
|
2476
|
if( nPending>0 ){ |
|
2477
|
@ <h1>%,d(nTotal) Subscribers, %,d(nPending) Pending</h1> |
|
2478
|
if( nDel==0 && 0<db_int(0,"SELECT count(*) FROM subscriber" |
|
2479
|
" WHERE NOT sverified AND mtime<now()-86400") |
|
2480
|
){ |
|
2481
|
style_submenu_element("Purge Pending","subscribers?purge"); |
|
2482
|
} |
|
2483
|
}else{ |
|
2484
|
@ <h1>%,d(nTotal) Subscribers</h1> |
|
2485
|
} |
|
2486
|
if( nDel>0 ){ |
|
2487
|
@ <p>*** %d(nDel) pending subscriptions deleted ***</p> |
|
2488
|
} |
|
2489
|
blob_init(&sql, 0, 0); |
|
2490
|
blob_append_sql(&sql, |
|
2491
|
"SELECT subscriberId," /* 0 */ |
|
2492
|
" semail," /* 1 */ |
|
2493
|
" ssub," /* 2 */ |
|
2494
|
" suname," /* 3 */ |
|
2495
|
" sverified," /* 4 */ |
|
2496
|
" sdigest," /* 5 */ |
|
2497
|
" mtime," /* 6 */ |
|
2498
|
" date(sctime,'unixepoch')," /* 7 */ |
|
2499
|
" (SELECT uid FROM user WHERE login=subscriber.suname)," /* 8 */ |
|
2500
|
" coalesce(lastContact,mtime/86400)" /* 9 */ |
|
2501
|
" FROM subscriber" |
|
2502
|
); |
|
2503
|
if( P("only")!=0 ){ |
|
2504
|
blob_append_sql(&sql, " WHERE ssub LIKE '%%%q%%'", P("only")); |
|
2505
|
style_submenu_element("Show All","%R/subscribers"); |
|
2506
|
} |
|
2507
|
blob_append_sql(&sql," ORDER BY mtime DESC"); |
|
2508
|
db_prepare_blob(&q, &sql); |
|
2509
|
iNow = time(0); |
|
2510
|
memcpy(zCutoffClr, hash_color("A"), sizeof(zCutoffClr)); |
|
2511
|
memcpy(zWarnClr, hash_color("HIJ"), sizeof(zWarnClr)); |
|
2512
|
@ <table border='1' class='sortable' \ |
|
2513
|
@ data-init-sort='6' data-column-types='tttttKKt'> |
|
2514
|
@ <thead> |
|
2515
|
@ <tr> |
|
2516
|
@ <th>Email |
|
2517
|
@ <th>Events |
|
2518
|
@ <th>Digest-Only? |
|
2519
|
@ <th>User |
|
2520
|
@ <th>Verified? |
|
2521
|
@ <th>Last change |
|
2522
|
@ <th>Last contact |
|
2523
|
@ <th>Created |
|
2524
|
@ </tr> |
|
2525
|
@ </thead><tbody> |
|
2526
|
while( db_step(&q)==SQLITE_ROW ){ |
|
2527
|
sqlite3_int64 iMtime = db_column_int64(&q, 6); |
|
2528
|
double rAge = (iNow - iMtime)/86400.0; |
|
2529
|
int uid = db_column_int(&q, 8); |
|
2530
|
const char *zUname = db_column_text(&q, 3); |
|
2531
|
sqlite3_int64 iContact = db_column_int64(&q, 9); |
|
2532
|
double rContact = (iNow/86400.0) - iContact; |
|
2533
|
@ <tr> |
|
2534
|
@ <td><a href='%R/alerts?sid=%d(db_column_int(&q,0))'>\ |
|
2535
|
@ %h(db_column_text(&q,1))</a></td> |
|
2536
|
@ <td>%h(db_column_text(&q,2))</td> |
|
2537
|
@ <td>%s(db_column_int(&q,5)?"digest":"")</td> |
|
2538
|
if( uid ){ |
|
2539
|
@ <td><a href='%R/setup_uedit?id=%d(uid)'>%h(zUname)</a> |
|
2540
|
}else{ |
|
2541
|
@ <td>%h(zUname)</td> |
|
2542
|
} |
|
2543
|
@ <td>%s(db_column_int(&q,4)?"yes":"pending")</td> |
|
2544
|
@ <td data-sortkey='%010llx(iMtime)'>%z(human_readable_age(rAge))</td> |
|
2545
|
@ <td data-sortkey='%010llx(iContact)'>\ |
|
2546
|
if( iContact>iWarning ){ |
|
2547
|
@ <span>\ |
|
2548
|
}else if( iContact>iCutoff ){ |
|
2549
|
@ <span style='background-color:%s(zWarnClr);'>\ |
|
2550
|
}else{ |
|
2551
|
@ <span style='background-color:%s(zCutoffClr);'>\ |
|
2552
|
} |
|
2553
|
@ %z(human_readable_age(rContact))</td> |
|
2554
|
@ <td>%h(db_column_text(&q,7))</td> |
|
2555
|
@ </tr> |
|
2556
|
} |
|
2557
|
@ </tbody></table> |
|
2558
|
db_finalize(&q); |
|
2559
|
style_table_sorter(); |
|
2560
|
style_finish_page(); |
|
2561
|
} |
|
2562
|
|
|
2563
|
#if LOCAL_INTERFACE |
|
2564
|
/* |
|
2565
|
** A single event that might appear in an alert is recorded as an |
|
2566
|
** instance of the following object. |
|
2567
|
** |
|
2568
|
** type values: |
|
2569
|
** |
|
2570
|
** c A new check-in |
|
2571
|
** f An original forum post |
|
2572
|
** n New forum threads |
|
2573
|
** r Replies to my forum posts |
|
2574
|
** x An edit to a prior forum post |
|
2575
|
** t A new ticket or a change to an existing ticket |
|
2576
|
** u A user was added or received new permissions |
|
2577
|
** w A change to a wiki page |
|
2578
|
** x Edits to forum posts |
|
2579
|
*/ |
|
2580
|
struct EmailEvent { |
|
2581
|
int type; /* 'c', 'f', 'n', 'r', 't', 'u', 'w', 'x' */ |
|
2582
|
int needMod; /* Pending moderator approval */ |
|
2583
|
Blob hdr; /* Header content, for forum entries */ |
|
2584
|
Blob txt; /* Text description to appear in an alert */ |
|
2585
|
char *zFromName; /* Human name of the sender */ |
|
2586
|
char *zPriors; /* Upthread sender IDs for forum posts */ |
|
2587
|
EmailEvent *pNext; /* Next in chronological order */ |
|
2588
|
}; |
|
2589
|
#endif |
|
2590
|
|
|
2591
|
/* |
|
2592
|
** Free a linked list of EmailEvent objects |
|
2593
|
*/ |
|
2594
|
void alert_free_eventlist(EmailEvent *p){ |
|
2595
|
while( p ){ |
|
2596
|
EmailEvent *pNext = p->pNext; |
|
2597
|
blob_reset(&p->txt); |
|
2598
|
blob_reset(&p->hdr); |
|
2599
|
fossil_free(p->zFromName); |
|
2600
|
fossil_free(p->zPriors); |
|
2601
|
fossil_free(p); |
|
2602
|
p = pNext; |
|
2603
|
} |
|
2604
|
} |
|
2605
|
|
|
2606
|
/* |
|
2607
|
** Compute a string that is appropriate for the EmailEvent.zPriors field |
|
2608
|
** for a particular forum post. |
|
2609
|
** |
|
2610
|
** This string is an encode list of sender names and rids for all ancestors |
|
2611
|
** of the fpid post - the post that fpid answers, the post that parent |
|
2612
|
** post answers, and so forth back up to the root post. Duplicates sender |
|
2613
|
** names are omitted. |
|
2614
|
** |
|
2615
|
** The EmailEvent.zPriors field is used to screen events for people who |
|
2616
|
** only want to see replies to their own posts or to specific posts. |
|
2617
|
*/ |
|
2618
|
static char *alert_compute_priors(int fpid){ |
|
2619
|
return db_text(0, |
|
2620
|
"WITH priors(rid,who) AS (" |
|
2621
|
" SELECT firt, coalesce(euser,user)" |
|
2622
|
" FROM forumpost LEFT JOIN event ON fpid=objid" |
|
2623
|
" WHERE fpid=%d" |
|
2624
|
" UNION ALL" |
|
2625
|
" SELECT firt, coalesce(euser,user)" |
|
2626
|
" FROM priors, forumpost LEFT JOIN event ON fpid=objid" |
|
2627
|
" WHERE fpid=rid" |
|
2628
|
")" |
|
2629
|
"SELECT ','||group_concat(DISTINCT 'u'||who)||" |
|
2630
|
"','||group_concat(rid) FROM priors;", |
|
2631
|
fpid |
|
2632
|
); |
|
2633
|
} |
|
2634
|
|
|
2635
|
/* |
|
2636
|
** Compute and return a linked list of EmailEvent objects |
|
2637
|
** corresponding to the current content of the temp.wantalert |
|
2638
|
** table which should be defined as follows: |
|
2639
|
** |
|
2640
|
** CREATE TEMP TABLE wantalert(eventId TEXT, needMod BOOLEAN); |
|
2641
|
*/ |
|
2642
|
EmailEvent *alert_compute_event_text(int *pnEvent, int doDigest){ |
|
2643
|
Stmt q; |
|
2644
|
EmailEvent *p; |
|
2645
|
EmailEvent anchor; |
|
2646
|
EmailEvent *pLast; |
|
2647
|
const char *zUrl = db_get("email-url","http://localhost:8080"); |
|
2648
|
const char *zFrom; |
|
2649
|
const char *zSub; |
|
2650
|
|
|
2651
|
|
|
2652
|
/* First do non-forum post events */ |
|
2653
|
db_prepare(&q, |
|
2654
|
"SELECT" |
|
2655
|
" CASE WHEN event.type='t'" |
|
2656
|
" THEN (SELECT substr(tagname,5) FROM tag" |
|
2657
|
" WHERE tagid=event.tagid AND tagname LIKE 'tkt-%%')" |
|
2658
|
" ELSE blob.uuid END," /* 0 */ |
|
2659
|
" datetime(event.mtime)," /* 1 */ |
|
2660
|
" coalesce(ecomment,comment)" |
|
2661
|
" || ' (user: ' || coalesce(euser,user,'?')" |
|
2662
|
" || (SELECT case when length(x)>0 then ' tags: ' || x else '' end" |
|
2663
|
" FROM (SELECT group_concat(substr(tagname,5), ', ') AS x" |
|
2664
|
" FROM tag, tagxref" |
|
2665
|
" WHERE tagname GLOB 'sym-*' AND tag.tagid=tagxref.tagid" |
|
2666
|
" AND tagxref.rid=blob.rid AND tagxref.tagtype>0))" |
|
2667
|
" || ')' as comment," /* 2 */ |
|
2668
|
" wantalert.eventId," /* 3 */ |
|
2669
|
" wantalert.needMod" /* 4 */ |
|
2670
|
" FROM temp.wantalert, event, blob" |
|
2671
|
" WHERE blob.rid=event.objid" |
|
2672
|
" AND event.objid=substr(wantalert.eventId,2)+0" |
|
2673
|
" AND (%d OR eventId NOT GLOB 'f*')" |
|
2674
|
" ORDER BY event.mtime", |
|
2675
|
doDigest |
|
2676
|
); |
|
2677
|
memset(&anchor, 0, sizeof(anchor)); |
|
2678
|
pLast = &anchor; |
|
2679
|
*pnEvent = 0; |
|
2680
|
while( db_step(&q)==SQLITE_ROW ){ |
|
2681
|
const char *zType = ""; |
|
2682
|
const char *zComment = db_column_text(&q, 2); |
|
2683
|
p = fossil_malloc_zero( sizeof(EmailEvent) ); |
|
2684
|
pLast->pNext = p; |
|
2685
|
pLast = p; |
|
2686
|
p->type = db_column_text(&q, 3)[0]; |
|
2687
|
p->needMod = db_column_int(&q, 4); |
|
2688
|
p->zFromName = 0; |
|
2689
|
p->pNext = 0; |
|
2690
|
switch( p->type ){ |
|
2691
|
case 'c': zType = "Check-In"; break; |
|
2692
|
/* case 'f': -- forum posts omitted from this loop. See below */ |
|
2693
|
case 't': zType = "Ticket Change"; break; |
|
2694
|
case 'w': { |
|
2695
|
zType = "Wiki Edit"; |
|
2696
|
switch( zComment ? *zComment : 0 ){ |
|
2697
|
case ':': ++zComment; break; |
|
2698
|
case '+': zType = "Wiki Added"; ++zComment; break; |
|
2699
|
case '-': zType = "Wiki Removed"; ++zComment; break; |
|
2700
|
} |
|
2701
|
break; |
|
2702
|
} |
|
2703
|
} |
|
2704
|
blob_init(&p->hdr, 0, 0); |
|
2705
|
blob_init(&p->txt, 0, 0); |
|
2706
|
blob_appendf(&p->txt,"== %s %s ==\n%s\n%s/info/%.20s\n", |
|
2707
|
db_column_text(&q,1), |
|
2708
|
zType, |
|
2709
|
zComment, |
|
2710
|
zUrl, |
|
2711
|
db_column_text(&q,0) |
|
2712
|
); |
|
2713
|
if( p->needMod ){ |
|
2714
|
blob_appendf(&p->txt, |
|
2715
|
"** Pending moderator approval (%s/modreq) **\n", |
|
2716
|
zUrl |
|
2717
|
); |
|
2718
|
} |
|
2719
|
(*pnEvent)++; |
|
2720
|
} |
|
2721
|
db_finalize(&q); |
|
2722
|
|
|
2723
|
/* Early-out if forumpost is not a table in this repository */ |
|
2724
|
if( !db_table_exists("repository","forumpost") ){ |
|
2725
|
return anchor.pNext; |
|
2726
|
} |
|
2727
|
|
|
2728
|
/* For digests, the previous loop also handled forumposts already */ |
|
2729
|
if( doDigest ){ |
|
2730
|
return anchor.pNext; |
|
2731
|
} |
|
2732
|
|
|
2733
|
/* If we reach this point, it means that forumposts exist and this |
|
2734
|
** is a normal email alert. Construct full-text forum post alerts |
|
2735
|
** using a format that enables them to be sent as separate emails. |
|
2736
|
*/ |
|
2737
|
db_prepare(&q, |
|
2738
|
"SELECT" |
|
2739
|
" forumpost.fpid," /* 0: fpid */ |
|
2740
|
" (SELECT uuid FROM blob WHERE rid=forumpost.fpid)," /* 1: hash */ |
|
2741
|
" datetime(event.mtime)," /* 2: date/time */ |
|
2742
|
" substr(comment,instr(comment,':')+2)," /* 3: comment */ |
|
2743
|
" (WITH thread(fpid,fprev) AS (" |
|
2744
|
" SELECT fpid,fprev FROM forumpost AS tx" |
|
2745
|
" WHERE tx.froot=forumpost.froot)," |
|
2746
|
" basepid(fpid,bpid) AS (" |
|
2747
|
" SELECT fpid, fpid FROM thread WHERE fprev IS NULL" |
|
2748
|
" UNION ALL" |
|
2749
|
" SELECT thread.fpid, basepid.bpid FROM basepid, thread" |
|
2750
|
" WHERE basepid.fpid=thread.fprev)" |
|
2751
|
" SELECT uuid FROM blob, basepid" |
|
2752
|
" WHERE basepid.fpid=forumpost.firt" |
|
2753
|
" AND blob.rid=basepid.bpid)," /* 4: in-reply-to */ |
|
2754
|
" wantalert.needMod," /* 5: moderated */ |
|
2755
|
" coalesce(display_name(info),euser,user)," /* 6: user */ |
|
2756
|
" forumpost.fprev IS NULL" /* 7: is an edit */ |
|
2757
|
" FROM temp.wantalert, event, forumpost" |
|
2758
|
" LEFT JOIN user ON (login=coalesce(euser,user))" |
|
2759
|
" WHERE event.objid=substr(wantalert.eventId,2)+0" |
|
2760
|
" AND eventId GLOB 'f*'" |
|
2761
|
" AND forumpost.fpid=event.objid" |
|
2762
|
" ORDER BY event.mtime" |
|
2763
|
); |
|
2764
|
zFrom = db_get("email-self",0); |
|
2765
|
zSub = db_get("email-subname",""); |
|
2766
|
while( db_step(&q)==SQLITE_ROW ){ |
|
2767
|
int fpid = db_column_int(&q,0); |
|
2768
|
Manifest *pPost = manifest_get(fpid, CFTYPE_FORUM, 0); |
|
2769
|
const char *zIrt; |
|
2770
|
const char *zUuid; |
|
2771
|
const char *zTitle; |
|
2772
|
const char *z; |
|
2773
|
if( pPost==0 ) continue; |
|
2774
|
p = fossil_malloc( sizeof(EmailEvent) ); |
|
2775
|
pLast->pNext = p; |
|
2776
|
pLast = p; |
|
2777
|
p->type = db_column_int(&q,7) ? 'f' : 'x'; |
|
2778
|
p->needMod = db_column_int(&q, 5); |
|
2779
|
z = db_column_text(&q,6); |
|
2780
|
p->zFromName = z && z[0] ? fossil_strdup(z) : 0; |
|
2781
|
p->zPriors = alert_compute_priors(fpid); |
|
2782
|
p->pNext = 0; |
|
2783
|
blob_init(&p->hdr, 0, 0); |
|
2784
|
zUuid = db_column_text(&q, 1); |
|
2785
|
zTitle = db_column_text(&q, 3); |
|
2786
|
if( p->needMod ){ |
|
2787
|
blob_appendf(&p->hdr, "Subject: %s Pending Moderation: %s\r\n", |
|
2788
|
zSub, zTitle); |
|
2789
|
}else{ |
|
2790
|
blob_appendf(&p->hdr, "Subject: %s %s\r\n", zSub, zTitle); |
|
2791
|
blob_appendf(&p->hdr, "Message-Id: <%.32s@%s>\r\n", |
|
2792
|
zUuid, alert_hostname(zFrom)); |
|
2793
|
zIrt = db_column_text(&q, 4); |
|
2794
|
if( zIrt && zIrt[0] ){ |
|
2795
|
blob_appendf(&p->hdr, "In-Reply-To: <%.32s@%s>\r\n", |
|
2796
|
zIrt, alert_hostname(zFrom)); |
|
2797
|
} |
|
2798
|
} |
|
2799
|
blob_init(&p->txt, 0, 0); |
|
2800
|
if( p->needMod ){ |
|
2801
|
blob_appendf(&p->txt, |
|
2802
|
"** Pending moderator approval (%s/modreq) **\n", |
|
2803
|
zUrl |
|
2804
|
); |
|
2805
|
} |
|
2806
|
blob_appendf(&p->txt, |
|
2807
|
"Forum post by %s on %s\n", |
|
2808
|
pPost->zUser, db_column_text(&q, 2)); |
|
2809
|
blob_appendf(&p->txt, "%s/forumpost/%S\n\n", zUrl, zUuid); |
|
2810
|
blob_append(&p->txt, pPost->zWiki, -1); |
|
2811
|
manifest_destroy(pPost); |
|
2812
|
(*pnEvent)++; |
|
2813
|
} |
|
2814
|
db_finalize(&q); |
|
2815
|
|
|
2816
|
return anchor.pNext; |
|
2817
|
} |
|
2818
|
|
|
2819
|
/* |
|
2820
|
** Put a header on an alert email |
|
2821
|
*/ |
|
2822
|
void email_header(Blob *pOut){ |
|
2823
|
blob_appendf(pOut, |
|
2824
|
"This is an automated email reporting changes " |
|
2825
|
"on Fossil repository %s (%s/timeline)\n", |
|
2826
|
db_get("email-subname","(unknown)"), |
|
2827
|
db_get("email-url","http://localhost:8080")); |
|
2828
|
} |
|
2829
|
|
|
2830
|
/* |
|
2831
|
** COMMAND: test-alert |
|
2832
|
** |
|
2833
|
** Usage: %fossil test-alert EVENTID ... |
|
2834
|
** |
|
2835
|
** Generate the text of an email alert for all of the EVENTIDs |
|
2836
|
** listed on the command-line. Or if no events are listed on the |
|
2837
|
** command line, generate text for all events named in the |
|
2838
|
** pending_alert table. The text of the email alerts appears on |
|
2839
|
** standard output. |
|
2840
|
** |
|
2841
|
** This command is intended for testing and debugging Fossil itself, |
|
2842
|
** for example when enhancing the email alert system or fixing bugs |
|
2843
|
** in the email alert system. If you are not making changes to the |
|
2844
|
** Fossil source code, this command is probably not useful to you. |
|
2845
|
** |
|
2846
|
** EVENTIDs are text. The first character is 'c', 'f', 't', or 'w' |
|
2847
|
** for check-in, forum, ticket, or wiki. The remaining text is a |
|
2848
|
** integer that references the EVENT.OBJID value for the event. |
|
2849
|
** Run /timeline?showid to see these OBJID values. |
|
2850
|
** |
|
2851
|
** Options: |
|
2852
|
** --digest Generate digest alert text |
|
2853
|
** --needmod Assume all events are pending moderator approval |
|
2854
|
*/ |
|
2855
|
void test_alert_cmd(void){ |
|
2856
|
Blob out; |
|
2857
|
int nEvent; |
|
2858
|
int needMod; |
|
2859
|
int doDigest; |
|
2860
|
EmailEvent *pEvent, *p; |
|
2861
|
|
|
2862
|
doDigest = find_option("digest",0,0)!=0; |
|
2863
|
needMod = find_option("needmod",0,0)!=0; |
|
2864
|
db_find_and_open_repository(0, 0); |
|
2865
|
verify_all_options(); |
|
2866
|
db_begin_transaction(); |
|
2867
|
alert_schema(0); |
|
2868
|
db_multi_exec("CREATE TEMP TABLE wantalert(eventid TEXT, needMod BOOLEAN)"); |
|
2869
|
if( g.argc==2 ){ |
|
2870
|
db_multi_exec( |
|
2871
|
"INSERT INTO wantalert(eventId,needMod)" |
|
2872
|
" SELECT eventid, %d FROM pending_alert", needMod); |
|
2873
|
}else{ |
|
2874
|
int i; |
|
2875
|
for(i=2; i<g.argc; i++){ |
|
2876
|
db_multi_exec("INSERT INTO wantalert(eventId,needMod) VALUES(%Q,%d)", |
|
2877
|
g.argv[i], needMod); |
|
2878
|
} |
|
2879
|
} |
|
2880
|
blob_init(&out, 0, 0); |
|
2881
|
email_header(&out); |
|
2882
|
pEvent = alert_compute_event_text(&nEvent, doDigest); |
|
2883
|
for(p=pEvent; p; p=p->pNext){ |
|
2884
|
blob_append(&out, "\n", 1); |
|
2885
|
if( blob_size(&p->hdr) ){ |
|
2886
|
blob_append(&out, blob_buffer(&p->hdr), blob_size(&p->hdr)); |
|
2887
|
blob_append(&out, "\n", 1); |
|
2888
|
} |
|
2889
|
blob_append(&out, blob_buffer(&p->txt), blob_size(&p->txt)); |
|
2890
|
} |
|
2891
|
alert_free_eventlist(pEvent); |
|
2892
|
fossil_print("%s", blob_str(&out)); |
|
2893
|
blob_reset(&out); |
|
2894
|
db_end_transaction(0); |
|
2895
|
} |
|
2896
|
|
|
2897
|
/* |
|
2898
|
** COMMAND: test-add-alerts |
|
2899
|
** |
|
2900
|
** Usage: %fossil test-add-alerts [OPTIONS] EVENTID ... |
|
2901
|
** |
|
2902
|
** Add one or more events to the pending_alert queue. Use this |
|
2903
|
** command during testing to force email notifications for specific |
|
2904
|
** events. |
|
2905
|
** |
|
2906
|
** EVENTIDs are text. The first character is 'c', 'f', 't', or 'w' |
|
2907
|
** for check-in, forum, ticket, or wiki. The remaining text is a |
|
2908
|
** integer that references the EVENT.OBJID value for the event. |
|
2909
|
** Run /timeline?showid to see these OBJID values. |
|
2910
|
** |
|
2911
|
** Options: |
|
2912
|
** --backoffice Run alert_backoffice() after all alerts have |
|
2913
|
** been added. This will cause the alerts to be |
|
2914
|
** sent out with the SENDALERT_TRACE option. |
|
2915
|
** --debug Like --backoffice, but add the SENDALERT_STDOUT |
|
2916
|
** so that emails are printed to standard output |
|
2917
|
** rather than being sent. |
|
2918
|
** --digest Process emails using SENDALERT_DIGEST |
|
2919
|
*/ |
|
2920
|
void test_add_alert_cmd(void){ |
|
2921
|
int i; |
|
2922
|
int doAuto = find_option("backoffice",0,0)!=0; |
|
2923
|
unsigned mFlags = 0; |
|
2924
|
if( find_option("debug",0,0)!=0 ){ |
|
2925
|
doAuto = 1; |
|
2926
|
mFlags = SENDALERT_STDOUT; |
|
2927
|
} |
|
2928
|
if( find_option("digest",0,0)!=0 ){ |
|
2929
|
mFlags |= SENDALERT_DIGEST; |
|
2930
|
} |
|
2931
|
db_find_and_open_repository(0, 0); |
|
2932
|
verify_all_options(); |
|
2933
|
db_begin_write(); |
|
2934
|
alert_schema(0); |
|
2935
|
for(i=2; i<g.argc; i++){ |
|
2936
|
db_multi_exec("REPLACE INTO pending_alert(eventId) VALUES(%Q)", g.argv[i]); |
|
2937
|
} |
|
2938
|
db_end_transaction(0); |
|
2939
|
if( doAuto ){ |
|
2940
|
alert_backoffice(SENDALERT_TRACE|mFlags); |
|
2941
|
} |
|
2942
|
} |
|
2943
|
|
|
2944
|
/* |
|
2945
|
** Minimum number of days between renewal messages |
|
2946
|
*/ |
|
2947
|
#define ALERT_RENEWAL_MSG_FREQUENCY 7 /* Do renewals at most once/week */ |
|
2948
|
|
|
2949
|
/* |
|
2950
|
** Construct the header and body for an email message that will alert |
|
2951
|
** a subscriber that their subscriptions are about to expire. |
|
2952
|
*/ |
|
2953
|
static void alert_renewal_msg( |
|
2954
|
Blob *pHdr, /* Write email header here */ |
|
2955
|
Blob *pBody, /* Write email body here */ |
|
2956
|
const char *zCode, /* The subscriber code */ |
|
2957
|
int lastContact, /* Last contact (days since 1970) */ |
|
2958
|
const char *zEAddr, /* Subscriber email address. Send to this. */ |
|
2959
|
const char *zSub, /* Subscription codes */ |
|
2960
|
const char *zRepoName, /* Name of the sending Fossil repository */ |
|
2961
|
const char *zUrl /* URL for the sending Fossil repository */ |
|
2962
|
){ |
|
2963
|
blob_appendf(pHdr,"To: <%s>\r\n", zEAddr); |
|
2964
|
blob_appendf(pHdr,"Subject: %s Subscription to %s expires soon\r\n", |
|
2965
|
zRepoName, zUrl); |
|
2966
|
blob_appendf(pBody, |
|
2967
|
"\nTo renew your subscription, click the following link:\n" |
|
2968
|
"\n %s/renew/%s\n\n", |
|
2969
|
zUrl, zCode |
|
2970
|
); |
|
2971
|
blob_appendf(pBody, |
|
2972
|
"You are currently receiving email notification for the following events\n" |
|
2973
|
"on the %s Fossil repository at %s:\n\n", |
|
2974
|
zRepoName, zUrl |
|
2975
|
); |
|
2976
|
if( strchr(zSub, 'a') ) blob_appendf(pBody, " * Announcements\n"); |
|
2977
|
if( strchr(zSub, 'c') ) blob_appendf(pBody, " * Check-ins\n"); |
|
2978
|
if( strchr(zSub, 'f') ) blob_appendf(pBody, " * Forum posts\n"); |
|
2979
|
if( strchr(zSub, 't') ) blob_appendf(pBody, " * Ticket changes\n"); |
|
2980
|
if( strchr(zSub, 'u') ) blob_appendf(pBody, " * User permission elevation\n"); |
|
2981
|
if( strchr(zSub, 'w') ) blob_appendf(pBody, " * Wiki changes\n"); |
|
2982
|
blob_appendf(pBody, "\n" |
|
2983
|
"If you take no action, your subscription will expire and you will be\n" |
|
2984
|
"unsubscribed in about %d days. To make other changes or to unsubscribe\n" |
|
2985
|
"immediately, visit the following webpage:\n\n" |
|
2986
|
" %s/alerts/%s\n\n", |
|
2987
|
ALERT_RENEWAL_MSG_FREQUENCY, zUrl, zCode |
|
2988
|
); |
|
2989
|
} |
|
2990
|
|
|
2991
|
/* |
|
2992
|
** If zUser is a sender of one of the ancestors of a forum post |
|
2993
|
** (if zUser appears in zPriors) then return true. |
|
2994
|
*/ |
|
2995
|
static int alert_in_priors(const char *zUser, const char *zPriors){ |
|
2996
|
int n = (int)strlen(zUser); |
|
2997
|
char zBuf[200]; |
|
2998
|
if( n>195 ) return 0; |
|
2999
|
if( zPriors==0 || zPriors[0]==0 ) return 0; |
|
3000
|
zBuf[0] = ','; |
|
3001
|
zBuf[1] = 'u'; |
|
3002
|
memcpy(zBuf+2, zUser, n+1); |
|
3003
|
return strstr(zPriors, zBuf)!=0; |
|
3004
|
} |
|
3005
|
|
|
3006
|
#if INTERFACE |
|
3007
|
/* |
|
3008
|
** Flags for alert_send_alerts() |
|
3009
|
*/ |
|
3010
|
#define SENDALERT_DIGEST 0x0001 /* Send a digest */ |
|
3011
|
#define SENDALERT_PRESERVE 0x0002 /* Do not mark the task as done */ |
|
3012
|
#define SENDALERT_STDOUT 0x0004 /* Print emails instead of sending */ |
|
3013
|
#define SENDALERT_TRACE 0x0008 /* Trace operation for debugging */ |
|
3014
|
#define SENDALERT_RENEWAL 0x0010 /* Send renewal notices */ |
|
3015
|
|
|
3016
|
#endif /* INTERFACE */ |
|
3017
|
|
|
3018
|
/* |
|
3019
|
** Send alert emails to subscribers. |
|
3020
|
** |
|
3021
|
** This procedure is run by either the backoffice, or in response to the |
|
3022
|
** "fossil alerts send" command. Details of operation are controlled by |
|
3023
|
** the flags parameter. |
|
3024
|
** |
|
3025
|
** Here is a summary of what happens: |
|
3026
|
** |
|
3027
|
** (1) Create a TEMP table wantalert(eventId,needMod) and fill it with |
|
3028
|
** all the events that we want to send alerts about. The needMod |
|
3029
|
** flags is set if and only if the event is still awaiting |
|
3030
|
** moderator approval. Events with the needMod flag are only |
|
3031
|
** shown to users that have moderator privileges. |
|
3032
|
** |
|
3033
|
** (2) Call alert_compute_event_text() to compute a list of EmailEvent |
|
3034
|
** objects that describe all events about which we want to send |
|
3035
|
** alerts. |
|
3036
|
** |
|
3037
|
** (3) Loop over all subscribers. Compose and send one or more email |
|
3038
|
** messages to each subscriber that describe the events for |
|
3039
|
** which the subscriber has expressed interest and has |
|
3040
|
** appropriate privileges. |
|
3041
|
** |
|
3042
|
** (4) Update the pending_alerts table to indicate that alerts have been |
|
3043
|
** sent. |
|
3044
|
** |
|
3045
|
** Update 2018-08-09: Do step (3) before step (4). Update the |
|
3046
|
** pending_alerts table *before* the emails are sent. That way, if |
|
3047
|
** the process malfunctions or crashes, some notifications may never |
|
3048
|
** be sent. But that is better than some recurring bug causing |
|
3049
|
** subscribers to be flooded with repeated notifications every 60 |
|
3050
|
** seconds! |
|
3051
|
*/ |
|
3052
|
int alert_send_alerts(u32 flags){ |
|
3053
|
EmailEvent *pEvents, *p; |
|
3054
|
int nEvent = 0; |
|
3055
|
int nSent = 0; |
|
3056
|
Stmt q; |
|
3057
|
const char *zDigest = "false"; |
|
3058
|
Blob hdr, body; |
|
3059
|
const char *zUrl; |
|
3060
|
const char *zRepoName; |
|
3061
|
const char *zFrom; |
|
3062
|
const char *zDest = (flags & SENDALERT_STDOUT) ? "stdout" : 0; |
|
3063
|
AlertSender *pSender = 0; |
|
3064
|
u32 senderFlags = 0; |
|
3065
|
int iInterval = 0; /* Subscription renewal interval */ |
|
3066
|
|
|
3067
|
if( g.fSqlTrace ) fossil_trace("-- BEGIN alert_send_alerts(%u)\n", flags); |
|
3068
|
alert_schema(0); |
|
3069
|
if( !alert_enabled() && (flags & SENDALERT_STDOUT)==0 ) goto send_alert_done; |
|
3070
|
zUrl = db_get("email-url",0); |
|
3071
|
if( zUrl==0 ) goto send_alert_done; |
|
3072
|
zRepoName = db_get("email-subname",0); |
|
3073
|
if( zRepoName==0 ) goto send_alert_done; |
|
3074
|
zFrom = db_get("email-self",0); |
|
3075
|
if( zFrom==0 ) goto send_alert_done; |
|
3076
|
if( flags & SENDALERT_TRACE ){ |
|
3077
|
senderFlags |= ALERT_TRACE; |
|
3078
|
} |
|
3079
|
pSender = alert_sender_new(zDest, senderFlags); |
|
3080
|
|
|
3081
|
/* Step (1): Compute the alerts that need sending |
|
3082
|
*/ |
|
3083
|
db_multi_exec( |
|
3084
|
"DROP TABLE IF EXISTS temp.wantalert;" |
|
3085
|
"CREATE TEMP TABLE wantalert(eventId TEXT, needMod BOOLEAN, sentMod);" |
|
3086
|
); |
|
3087
|
if( flags & SENDALERT_DIGEST ){ |
|
3088
|
/* Unmoderated changes are never sent as part of a digest */ |
|
3089
|
db_multi_exec( |
|
3090
|
"INSERT INTO wantalert(eventId,needMod)" |
|
3091
|
" SELECT eventid, 0" |
|
3092
|
" FROM pending_alert" |
|
3093
|
" WHERE sentDigest IS FALSE" |
|
3094
|
" AND NOT EXISTS(SELECT 1 FROM private WHERE rid=substr(eventid,2));" |
|
3095
|
); |
|
3096
|
zDigest = "true"; |
|
3097
|
}else{ |
|
3098
|
/* Immediate alerts might include events that are subject to |
|
3099
|
** moderator approval */ |
|
3100
|
db_multi_exec( |
|
3101
|
"INSERT INTO wantalert(eventId,needMod,sentMod)" |
|
3102
|
" SELECT eventid," |
|
3103
|
" EXISTS(SELECT 1 FROM private WHERE rid=substr(eventid,2))," |
|
3104
|
" sentMod" |
|
3105
|
" FROM pending_alert" |
|
3106
|
" WHERE sentSep IS FALSE;" |
|
3107
|
"DELETE FROM wantalert WHERE needMod AND sentMod;" |
|
3108
|
); |
|
3109
|
} |
|
3110
|
if( g.fSqlTrace ){ |
|
3111
|
fossil_trace("-- wantalert contains %d rows\n", |
|
3112
|
db_int(0, "SELECT count(*) FROM wantalert") |
|
3113
|
); |
|
3114
|
} |
|
3115
|
|
|
3116
|
/* Step 2: compute EmailEvent objects for every notification that |
|
3117
|
** needs sending. |
|
3118
|
*/ |
|
3119
|
pEvents = alert_compute_event_text(&nEvent, (flags & SENDALERT_DIGEST)!=0); |
|
3120
|
if( nEvent==0 ) goto send_alert_expiration_warnings; |
|
3121
|
|
|
3122
|
/* Step 4a: Update the pending_alerts table to designate the |
|
3123
|
** alerts as having all been sent. This is done *before* step (3) |
|
3124
|
** so that a crash will not cause alerts to be sent multiple times. |
|
3125
|
** Better a missed alert than being spammed with hundreds of alerts |
|
3126
|
** due to a bug. |
|
3127
|
*/ |
|
3128
|
if( (flags & SENDALERT_PRESERVE)==0 ){ |
|
3129
|
if( flags & SENDALERT_DIGEST ){ |
|
3130
|
db_multi_exec( |
|
3131
|
"UPDATE pending_alert SET sentDigest=true" |
|
3132
|
" WHERE eventid IN (SELECT eventid FROM wantalert);" |
|
3133
|
); |
|
3134
|
}else{ |
|
3135
|
db_multi_exec( |
|
3136
|
"UPDATE pending_alert SET sentSep=true" |
|
3137
|
" WHERE eventid IN (SELECT eventid FROM wantalert WHERE NOT needMod);" |
|
3138
|
"UPDATE pending_alert SET sentMod=true" |
|
3139
|
" WHERE eventid IN (SELECT eventid FROM wantalert WHERE needMod);" |
|
3140
|
); |
|
3141
|
} |
|
3142
|
} |
|
3143
|
|
|
3144
|
/* Step 3: Loop over subscribers. Send alerts |
|
3145
|
*/ |
|
3146
|
blob_init(&hdr, 0, 0); |
|
3147
|
blob_init(&body, 0, 0); |
|
3148
|
db_prepare(&q, |
|
3149
|
"SELECT" |
|
3150
|
" hex(subscriberCode)," /* 0 */ |
|
3151
|
" semail," /* 1 */ |
|
3152
|
" ssub," /* 2 */ |
|
3153
|
" fullcap(user.cap)," /* 3 */ |
|
3154
|
" suname" /* 4 */ |
|
3155
|
" FROM subscriber LEFT JOIN user ON (login=suname)" |
|
3156
|
" WHERE sverified" |
|
3157
|
" AND NOT sdonotcall" |
|
3158
|
" AND sdigest IS %s" |
|
3159
|
" AND coalesce(subscriber.lastContact*86400,subscriber.mtime)>=%d", |
|
3160
|
zDigest/*safe-for-%s*/, |
|
3161
|
db_get_int("email-renew-cutoff",0) |
|
3162
|
); |
|
3163
|
while( db_step(&q)==SQLITE_ROW ){ |
|
3164
|
const char *zCode = db_column_text(&q, 0); |
|
3165
|
const char *zSub = db_column_text(&q, 2); |
|
3166
|
const char *zEmail = db_column_text(&q, 1); |
|
3167
|
const char *zCap = db_column_text(&q, 3); |
|
3168
|
const char *zUser = db_column_text(&q, 4); |
|
3169
|
int nHit = 0; |
|
3170
|
for(p=pEvents; p; p=p->pNext){ |
|
3171
|
if( strchr(zSub,p->type)==0 ){ |
|
3172
|
if( p->type!='f' ) continue; |
|
3173
|
if( strchr(zSub,'n')!=0 && (p->zPriors==0 || p->zPriors[0]==0) ){ |
|
3174
|
/* New post: accepted */ |
|
3175
|
}else if( strchr(zSub,'r')!=0 && zUser!=0 |
|
3176
|
&& alert_in_priors(zUser, p->zPriors) ){ |
|
3177
|
/* A follow-up to a post written by the user: accept */ |
|
3178
|
}else{ |
|
3179
|
continue; |
|
3180
|
} |
|
3181
|
} |
|
3182
|
if( p->needMod ){ |
|
3183
|
/* For events that require moderator approval, only send an alert |
|
3184
|
** if the recipient is a moderator for that type of event. Setup |
|
3185
|
** and Admin users always get notified. */ |
|
3186
|
char xType = '*'; |
|
3187
|
if( strpbrk(zCap,"as")==0 ){ |
|
3188
|
switch( p->type ){ |
|
3189
|
case 'x': case 'f': |
|
3190
|
case 'n': case 'r': xType = '5'; break; |
|
3191
|
case 't': xType = 'q'; break; |
|
3192
|
case 'w': xType = 'l'; break; |
|
3193
|
/* Note: case 'u' is not handled here */ |
|
3194
|
} |
|
3195
|
if( strchr(zCap,xType)==0 ) continue; |
|
3196
|
} |
|
3197
|
}else if( strchr(zCap,'s')!=0 || strchr(zCap,'a')!=0 ){ |
|
3198
|
/* Setup and admin users can get any notification that does not |
|
3199
|
** require moderation */ |
|
3200
|
}else{ |
|
3201
|
/* Other users only see the alert if they have sufficient |
|
3202
|
** privilege to view the event itself */ |
|
3203
|
char xType = '*'; |
|
3204
|
switch( p->type ){ |
|
3205
|
case 'c': xType = 'o'; break; |
|
3206
|
case 'x': case 'f': |
|
3207
|
case 'n': case 'r': xType = '2'; break; |
|
3208
|
case 't': xType = 'r'; break; |
|
3209
|
case 'w': xType = 'j'; break; |
|
3210
|
/* Note: case 'u' is not handled here */ |
|
3211
|
} |
|
3212
|
if( strchr(zCap,xType)==0 ) continue; |
|
3213
|
} |
|
3214
|
if( blob_size(&p->hdr)>0 ){ |
|
3215
|
/* This alert should be sent as a separate email */ |
|
3216
|
Blob fhdr, fbody; |
|
3217
|
blob_init(&fhdr, 0, 0); |
|
3218
|
blob_appendf(&fhdr, "To: <%s>\r\n", zEmail); |
|
3219
|
blob_append(&fhdr, blob_buffer(&p->hdr), blob_size(&p->hdr)); |
|
3220
|
blob_init(&fbody, blob_buffer(&p->txt), blob_size(&p->txt)); |
|
3221
|
if( pSender->zListId && pSender->zListId[0] ){ |
|
3222
|
blob_appendf(&fhdr, "List-Id: %s\r\n", pSender->zListId); |
|
3223
|
blob_appendf(&fhdr, "List-Unsubscribe: <%s/oneclickunsub/%s>\r\n", |
|
3224
|
zUrl, zCode); |
|
3225
|
blob_appendf(&fhdr, |
|
3226
|
"List-Unsubscribe-Post: List-Unsubscribe=One-Click\r\n"); |
|
3227
|
blob_appendf(&fbody, "\n-- \nUnsubscribe: %s/unsubscribe/%s\n", |
|
3228
|
zUrl, zCode); |
|
3229
|
/* blob_appendf(&fbody, "Subscription settings: %s/alerts/%s\n", |
|
3230
|
** zUrl, zCode); */ |
|
3231
|
} |
|
3232
|
alert_send(pSender,&fhdr,&fbody,p->zFromName); |
|
3233
|
nSent++; |
|
3234
|
blob_reset(&fhdr); |
|
3235
|
blob_reset(&fbody); |
|
3236
|
}else{ |
|
3237
|
/* Events other than forum posts are gathered together into |
|
3238
|
** a single email message */ |
|
3239
|
if( nHit==0 ){ |
|
3240
|
blob_appendf(&hdr,"To: <%s>\r\n", zEmail); |
|
3241
|
blob_appendf(&hdr,"Subject: %s activity alert\r\n", zRepoName); |
|
3242
|
blob_appendf(&body, |
|
3243
|
"This is an automated email sent by the Fossil repository " |
|
3244
|
"at %s to report changes.\n", |
|
3245
|
zUrl |
|
3246
|
); |
|
3247
|
} |
|
3248
|
nHit++; |
|
3249
|
blob_append(&body, "\n", 1); |
|
3250
|
blob_append(&body, blob_buffer(&p->txt), blob_size(&p->txt)); |
|
3251
|
} |
|
3252
|
} |
|
3253
|
if( nHit==0 ) continue; |
|
3254
|
if( pSender->zListId && pSender->zListId[0] ){ |
|
3255
|
blob_appendf(&hdr, "List-Id: %s\r\n", pSender->zListId); |
|
3256
|
blob_appendf(&hdr, "List-Unsubscribe: <%s/oneclickunsub/%s>\r\n", |
|
3257
|
zUrl, zCode); |
|
3258
|
blob_appendf(&hdr, |
|
3259
|
"List-Unsubscribe-Post: List-Unsubscribe=One-Click\r\n"); |
|
3260
|
blob_appendf(&body,"\n-- \nSubscription info: %s/alerts/%s\n", |
|
3261
|
zUrl, zCode); |
|
3262
|
} |
|
3263
|
alert_send(pSender,&hdr,&body,0); |
|
3264
|
nSent++; |
|
3265
|
blob_truncate(&hdr, 0); |
|
3266
|
blob_truncate(&body, 0); |
|
3267
|
} |
|
3268
|
blob_reset(&hdr); |
|
3269
|
blob_reset(&body); |
|
3270
|
db_finalize(&q); |
|
3271
|
alert_free_eventlist(pEvents); |
|
3272
|
|
|
3273
|
/* Step 4b: Update the pending_alerts table to remove all of the |
|
3274
|
** alerts that have been completely sent. |
|
3275
|
*/ |
|
3276
|
db_multi_exec("DELETE FROM pending_alert WHERE sentDigest AND sentSep;"); |
|
3277
|
|
|
3278
|
/* Send renewal messages to subscribers whose subscriptions are about |
|
3279
|
** to expire. Only do this if: |
|
3280
|
** |
|
3281
|
** (1) email-renew-interval is 14 or greater (or in other words if |
|
3282
|
** subscription expiration is enabled). |
|
3283
|
** |
|
3284
|
** (2) The SENDALERT_RENEWAL flag is set |
|
3285
|
*/ |
|
3286
|
send_alert_expiration_warnings: |
|
3287
|
if( (flags & SENDALERT_RENEWAL)!=0 |
|
3288
|
&& (iInterval = db_get_int("email-renew-interval",0))>=14 |
|
3289
|
){ |
|
3290
|
int iNow = (int)(time(0)/86400); |
|
3291
|
int iOldWarn = db_get_int("email-renew-warning",0); |
|
3292
|
int iNewWarn = iNow - iInterval + ALERT_RENEWAL_MSG_FREQUENCY; |
|
3293
|
if( iNewWarn >= iOldWarn + ALERT_RENEWAL_MSG_FREQUENCY ){ |
|
3294
|
db_prepare(&q, |
|
3295
|
"SELECT" |
|
3296
|
" hex(subscriberCode)," /* 0 */ |
|
3297
|
" lastContact," /* 1 */ |
|
3298
|
" semail," /* 2 */ |
|
3299
|
" ssub" /* 3 */ |
|
3300
|
" FROM subscriber" |
|
3301
|
" WHERE lastContact<=%d AND lastContact>%d" |
|
3302
|
" AND NOT sdonotcall" |
|
3303
|
" AND length(sdigest)>0", |
|
3304
|
iNewWarn, iOldWarn |
|
3305
|
); |
|
3306
|
while( db_step(&q)==SQLITE_ROW ){ |
|
3307
|
Blob hdr, body; |
|
3308
|
const char *zCode = db_column_text(&q,0); |
|
3309
|
blob_init(&hdr, 0, 0); |
|
3310
|
blob_init(&body, 0, 0); |
|
3311
|
alert_renewal_msg(&hdr, &body, |
|
3312
|
zCode, |
|
3313
|
db_column_int(&q,1), |
|
3314
|
db_column_text(&q,2), |
|
3315
|
db_column_text(&q,3), |
|
3316
|
zRepoName, zUrl); |
|
3317
|
if( pSender->zListId && pSender->zListId[0] ){ |
|
3318
|
blob_appendf(&hdr, "List-Id: %s\r\n", pSender->zListId); |
|
3319
|
blob_appendf(&hdr, "List-Unsubscribe: <%s/oneclickunsub/%s>\r\n", |
|
3320
|
zUrl, zCode); |
|
3321
|
blob_appendf(&hdr, |
|
3322
|
"List-Unsubscribe-Post: List-Unsubscribe=One-Click\r\n"); |
|
3323
|
blob_appendf(&body, "\n-- \nUnsubscribe: %s/unsubscribe/%s\n", |
|
3324
|
zUrl, zCode); |
|
3325
|
} |
|
3326
|
alert_send(pSender,&hdr,&body,0); |
|
3327
|
blob_reset(&hdr); |
|
3328
|
blob_reset(&body); |
|
3329
|
} |
|
3330
|
db_finalize(&q); |
|
3331
|
if( (flags & SENDALERT_PRESERVE)==0 ){ |
|
3332
|
if( iOldWarn>0 ){ |
|
3333
|
db_set_int("email-renew-cutoff", iOldWarn, 0); |
|
3334
|
} |
|
3335
|
db_set_int("email-renew-warning", iNewWarn, 0); |
|
3336
|
} |
|
3337
|
} |
|
3338
|
} |
|
3339
|
|
|
3340
|
send_alert_done: |
|
3341
|
alert_sender_free(pSender); |
|
3342
|
if( g.fSqlTrace ) fossil_trace("-- END alert_send_alerts(%u)\n", flags); |
|
3343
|
return nSent; |
|
3344
|
} |
|
3345
|
|
|
3346
|
/* |
|
3347
|
** Do backoffice processing for email notifications. In other words, |
|
3348
|
** check to see if any email notifications need to occur, and then |
|
3349
|
** do them. |
|
3350
|
** |
|
3351
|
** This routine is intended to run in the background, after webpages. |
|
3352
|
** |
|
3353
|
** The mFlags option is zero or more of the SENDALERT_* flags. Normally |
|
3354
|
** this flag is zero, but the test-set-alert command sets it to |
|
3355
|
** SENDALERT_TRACE. |
|
3356
|
*/ |
|
3357
|
int alert_backoffice(u32 mFlags){ |
|
3358
|
int iJulianDay; |
|
3359
|
int nSent = 0; |
|
3360
|
if( !alert_tables_exist() ) return 0; |
|
3361
|
nSent = alert_send_alerts(mFlags); |
|
3362
|
iJulianDay = db_int(0, "SELECT julianday('now')"); |
|
3363
|
if( iJulianDay>db_get_int("email-last-digest",0) ){ |
|
3364
|
db_set_int("email-last-digest",iJulianDay,0); |
|
3365
|
nSent += alert_send_alerts(SENDALERT_DIGEST|SENDALERT_RENEWAL|mFlags); |
|
3366
|
} |
|
3367
|
return nSent; |
|
3368
|
} |
|
3369
|
|
|
3370
|
/* |
|
3371
|
** WEBPAGE: contact_admin |
|
3372
|
** |
|
3373
|
** A web-form to send an email message to the repository administrator, |
|
3374
|
** or (with appropriate permissions) to anybody. |
|
3375
|
*/ |
|
3376
|
void contact_admin_page(void){ |
|
3377
|
const char *zAdminEmail = db_get("email-admin",0); |
|
3378
|
unsigned int uSeed = 0; |
|
3379
|
const char *zDecoded; |
|
3380
|
char *zCaptcha = 0; |
|
3381
|
|
|
3382
|
login_check_credentials(); |
|
3383
|
style_set_current_feature("alerts"); |
|
3384
|
if( zAdminEmail==0 || zAdminEmail[0]==0 ){ |
|
3385
|
style_header("Outbound Email Disabled"); |
|
3386
|
@ <p>Outbound email is disabled on this repository |
|
3387
|
style_finish_page(); |
|
3388
|
return; |
|
3389
|
} |
|
3390
|
if( P("submit")!=0 |
|
3391
|
&& P("subject")!=0 |
|
3392
|
&& P("msg")!=0 |
|
3393
|
&& P("from")!=0 |
|
3394
|
&& cgi_csrf_safe(2) |
|
3395
|
&& captcha_is_correct(0) |
|
3396
|
){ |
|
3397
|
Blob hdr, body; |
|
3398
|
AlertSender *pSender = alert_sender_new(0,0); |
|
3399
|
blob_init(&hdr, 0, 0); |
|
3400
|
blob_appendf(&hdr, "To: <%s>\r\nSubject: %s administrator message\r\n", |
|
3401
|
zAdminEmail, db_get("email-subname","Fossil Repo")); |
|
3402
|
blob_init(&body, 0, 0); |
|
3403
|
blob_appendf(&body, "Message from [%s]\n", PT("from")/*safe-for-%s*/); |
|
3404
|
blob_appendf(&body, "Subject: [%s]\n\n", PT("subject")/*safe-for-%s*/); |
|
3405
|
blob_appendf(&body, "%s", PT("msg")/*safe-for-%s*/); |
|
3406
|
alert_send(pSender, &hdr, &body, 0); |
|
3407
|
style_header("Message Sent"); |
|
3408
|
if( pSender->zErr ){ |
|
3409
|
@ <h1>Internal Error</h1> |
|
3410
|
@ <p>The following error was reported by the system: |
|
3411
|
@ <blockquote><pre> |
|
3412
|
@ %h(pSender->zErr) |
|
3413
|
@ </pre></blockquote> |
|
3414
|
}else{ |
|
3415
|
@ <p>Your message has been sent to the repository administrator. |
|
3416
|
@ Thank you for your input.</p> |
|
3417
|
} |
|
3418
|
alert_sender_free(pSender); |
|
3419
|
style_finish_page(); |
|
3420
|
return; |
|
3421
|
} |
|
3422
|
if( captcha_needed() ){ |
|
3423
|
uSeed = captcha_seed(); |
|
3424
|
zDecoded = captcha_decode(uSeed, 0); |
|
3425
|
zCaptcha = captcha_render(zDecoded); |
|
3426
|
} |
|
3427
|
style_set_current_feature("alerts"); |
|
3428
|
style_header("Message To Administrator"); |
|
3429
|
form_begin(0, "%R/contact_admin"); |
|
3430
|
@ <p>Enter a message to the repository administrator below:</p> |
|
3431
|
@ <table class="subscribe"> |
|
3432
|
if( zCaptcha ){ |
|
3433
|
@ <tr> |
|
3434
|
@ <td class="form_label">Security Code:</td> |
|
3435
|
@ <td><input type="text" name="captcha" value="" size="10"> |
|
3436
|
captcha_speakit_button(uSeed, "Speak the code"); |
|
3437
|
@ <input type="hidden" name="captchaseed" value="%u(uSeed)"></td> |
|
3438
|
@ </tr> |
|
3439
|
} |
|
3440
|
@ <tr> |
|
3441
|
@ <td class="form_label">Your Email Address:</td> |
|
3442
|
@ <td><input type="text" name="from" value="%h(PT("from"))" size="30"></td> |
|
3443
|
@ </tr> |
|
3444
|
@ <tr> |
|
3445
|
@ <td class="form_label">Subject:</td> |
|
3446
|
@ <td><input type="text" name="subject" value="%h(PT("subject"))"\ |
|
3447
|
@ size="80"></td> |
|
3448
|
@ </tr> |
|
3449
|
@ <tr> |
|
3450
|
@ <td class="form_label">Message:</td> |
|
3451
|
@ <td><textarea name="msg" cols="80" rows="10" wrap="virtual">\ |
|
3452
|
@ %h(PT("msg"))</textarea> |
|
3453
|
@ </tr> |
|
3454
|
@ <tr> |
|
3455
|
@ <td></td> |
|
3456
|
@ <td><input type="submit" name="submit" value="Send Message"> |
|
3457
|
@ </tr> |
|
3458
|
@ </table> |
|
3459
|
if( zCaptcha ){ |
|
3460
|
@ <div class="captcha"><table class="captcha"><tr><td><pre class="captcha"> |
|
3461
|
@ %h(zCaptcha) |
|
3462
|
@ </pre> |
|
3463
|
@ Enter the 8 characters above in the "Security Code" box<br/> |
|
3464
|
@ </td></tr></table></div> |
|
3465
|
} |
|
3466
|
@ </form> |
|
3467
|
style_finish_page(); |
|
3468
|
} |
|
3469
|
|
|
3470
|
/* |
|
3471
|
** Send an announcement message described by query parameter. |
|
3472
|
** Permission to do this has already been verified. |
|
3473
|
*/ |
|
3474
|
static char *alert_send_announcement(void){ |
|
3475
|
AlertSender *pSender; |
|
3476
|
char *zErr; |
|
3477
|
const char *zTo = PT("to"); |
|
3478
|
char *zSubject = PT("subject"); |
|
3479
|
int bAll = PB("all"); |
|
3480
|
int bAA = PB("aa"); |
|
3481
|
int bMods = PB("mods"); |
|
3482
|
const char *zSub = db_get("email-subname", "[Fossil Repo]"); |
|
3483
|
const char *zName = P("name"); /* Debugging options */ |
|
3484
|
const char *zDest = 0; /* How to send the announcement */ |
|
3485
|
int bTest = 0; |
|
3486
|
Blob hdr, body; |
|
3487
|
|
|
3488
|
if( fossil_strcmp(zName, "test2")==0 ){ |
|
3489
|
bTest = 2; |
|
3490
|
zDest = "blob"; |
|
3491
|
}else if( fossil_strcmp(zName, "test3")==0 ){ |
|
3492
|
bTest = 3; |
|
3493
|
if( fossil_strcmp(db_get("email-send-method",""),"relay")==0 ){ |
|
3494
|
zDest = "debug-relay"; |
|
3495
|
} |
|
3496
|
} |
|
3497
|
blob_init(&body, 0, 0); |
|
3498
|
blob_init(&hdr, 0, 0); |
|
3499
|
blob_appendf(&body, "%s", PT("msg")/*safe-for-%s*/); |
|
3500
|
pSender = alert_sender_new(zDest, 0); |
|
3501
|
if( zTo[0] ){ |
|
3502
|
blob_appendf(&hdr, "To: <%s>\r\nSubject: %s %s\r\n", zTo, zSub, zSubject); |
|
3503
|
alert_send(pSender, &hdr, &body, 0); |
|
3504
|
} |
|
3505
|
if( bAll || bAA || bMods ){ |
|
3506
|
Stmt q; |
|
3507
|
int nUsed = blob_size(&body); |
|
3508
|
const char *zURL = db_get("email-url",0); |
|
3509
|
if( bAll ){ |
|
3510
|
db_prepare(&q, "SELECT semail, hex(subscriberCode) FROM subscriber " |
|
3511
|
" WHERE sverified AND NOT sdonotcall"); |
|
3512
|
}else if( bAA ){ |
|
3513
|
db_prepare(&q, "SELECT semail, hex(subscriberCode) FROM subscriber " |
|
3514
|
" WHERE sverified AND NOT sdonotcall" |
|
3515
|
" AND ssub LIKE '%%a%%'"); |
|
3516
|
}else if( bMods ){ |
|
3517
|
db_prepare(&q, |
|
3518
|
"SELECT semail, hex(subscriberCode)" |
|
3519
|
" FROM subscriber, user " |
|
3520
|
" WHERE sverified AND NOT sdonotcall" |
|
3521
|
" AND suname=login" |
|
3522
|
" AND fullcap(cap) GLOB '*5*'"); |
|
3523
|
} |
|
3524
|
while( db_step(&q)==SQLITE_ROW ){ |
|
3525
|
const char *zCode = db_column_text(&q, 1); |
|
3526
|
zTo = db_column_text(&q, 0); |
|
3527
|
blob_truncate(&hdr, 0); |
|
3528
|
blob_appendf(&hdr, "To: <%s>\r\nSubject: %s %s\r\n", zTo, zSub, zSubject); |
|
3529
|
if( zURL ){ |
|
3530
|
blob_truncate(&body, nUsed); |
|
3531
|
blob_appendf(&body,"\n-- \nSubscription info: %s/alerts/%s\n", |
|
3532
|
zURL, zCode); |
|
3533
|
} |
|
3534
|
alert_send(pSender, &hdr, &body, 0); |
|
3535
|
} |
|
3536
|
db_finalize(&q); |
|
3537
|
} |
|
3538
|
if( bTest && blob_size(&pSender->out) ){ |
|
3539
|
/* If the URL is "/announce/test2" then no email is actually sent. |
|
3540
|
** Instead, the text of the email that would have been sent is |
|
3541
|
** displayed in the result window. |
|
3542
|
** |
|
3543
|
** If the URL is "/announce/test3" and the email-send-method is "relay" |
|
3544
|
** then the announcement is sent as it normally would be, but a |
|
3545
|
** transcript of the SMTP conversation with the MTA is shown here. |
|
3546
|
*/ |
|
3547
|
blob_trim(&pSender->out); |
|
3548
|
@ <pre style='border: 2px solid blue; padding: 1ex;'> |
|
3549
|
@ %h(blob_str(&pSender->out)) |
|
3550
|
@ </pre> |
|
3551
|
blob_reset(&pSender->out); |
|
3552
|
} |
|
3553
|
zErr = pSender->zErr; |
|
3554
|
pSender->zErr = 0; |
|
3555
|
alert_sender_free(pSender); |
|
3556
|
return zErr; |
|
3557
|
} |
|
3558
|
|
|
3559
|
|
|
3560
|
/* |
|
3561
|
** WEBPAGE: announce |
|
3562
|
** |
|
3563
|
** A web-form, available to users with the "Send-Announcement" or "A" |
|
3564
|
** capability, that allows one to send announcements to whomever |
|
3565
|
** has subscribed to receive announcements. The administrator can |
|
3566
|
** also send a message to an arbitrary email address and/or to all |
|
3567
|
** subscribers regardless of whether or not they have elected to |
|
3568
|
** receive announcements. |
|
3569
|
*/ |
|
3570
|
void announce_page(void){ |
|
3571
|
const char *zAction = "announce"; |
|
3572
|
const char *zName = PD("name",""); |
|
3573
|
/* |
|
3574
|
** Debugging Notes: |
|
3575
|
** |
|
3576
|
** /announce/test1 -> Shows query parameter values |
|
3577
|
** /announce/test2 -> Shows the formatted message but does |
|
3578
|
** not send it. |
|
3579
|
** /announce/test3 -> Sends the message, but also shows |
|
3580
|
** the SMTP transcript. |
|
3581
|
*/ |
|
3582
|
login_check_credentials(); |
|
3583
|
if( !g.perm.Announce ){ |
|
3584
|
login_needed(0); |
|
3585
|
return; |
|
3586
|
} |
|
3587
|
if( !g.perm.Setup ){ |
|
3588
|
zName = 0; /* Disable debugging feature for non-admin users */ |
|
3589
|
} |
|
3590
|
style_set_current_feature("alerts"); |
|
3591
|
if( fossil_strcmp(zName,"test1")==0 ){ |
|
3592
|
/* Visit the /announce/test1 page to see the CGI variables */ |
|
3593
|
zAction = "announce/test1"; |
|
3594
|
@ <p style='border: 1px solid black; padding: 1ex;'> |
|
3595
|
cgi_print_all(0, 0, 0); |
|
3596
|
@ </p> |
|
3597
|
}else if( P("submit")!=0 && cgi_csrf_safe(2) ){ |
|
3598
|
char *zErr = alert_send_announcement(); |
|
3599
|
style_header("Announcement Sent"); |
|
3600
|
if( zErr ){ |
|
3601
|
@ <h1>Error</h1> |
|
3602
|
@ <p>The following error was reported by the |
|
3603
|
@ announcement-sending subsystem: |
|
3604
|
@ <blockquote><pre> |
|
3605
|
@ %h(zErr) |
|
3606
|
@ </pre></blockquote> |
|
3607
|
}else{ |
|
3608
|
@ <p>The announcement has been sent. |
|
3609
|
@ <a href="%h(PD("REQUEST_URI","/"))">Send another</a></p> |
|
3610
|
} |
|
3611
|
style_finish_page(); |
|
3612
|
return; |
|
3613
|
} else if( !alert_enabled() ){ |
|
3614
|
style_header("Cannot Send Announcement"); |
|
3615
|
@ <p>Either you have no subscribers yet, or email alerts are not yet |
|
3616
|
@ <a href="https://fossil-scm.org/fossil/doc/trunk/www/alerts.md">set up</a> |
|
3617
|
@ for this repository.</p> |
|
3618
|
return; |
|
3619
|
} |
|
3620
|
|
|
3621
|
style_header("Send Announcement"); |
|
3622
|
alert_submenu_common(); |
|
3623
|
if( fossil_strcmp(zName,"test2")==0 ){ |
|
3624
|
zAction = "announce/test2"; |
|
3625
|
}else if( fossil_strcmp(zName,"test3")==0 ){ |
|
3626
|
zAction = "announce/test3"; |
|
3627
|
} |
|
3628
|
@ <form method="POST" action="%R/%s(zAction)"> |
|
3629
|
login_insert_csrf_secret(); |
|
3630
|
@ <table class="subscribe"> |
|
3631
|
if( g.perm.Admin ){ |
|
3632
|
int aa = PB("aa"); |
|
3633
|
int all = PB("all"); |
|
3634
|
int aMod = PB("mods"); |
|
3635
|
const char *aack = aa ? "checked" : ""; |
|
3636
|
const char *allck = all ? "checked" : ""; |
|
3637
|
const char *modck = aMod ? "checked" : ""; |
|
3638
|
@ <tr> |
|
3639
|
@ <td class="form_label">To:</td> |
|
3640
|
@ <td><input type="text" name="to" value="%h(PT("to"))" size="30"><br> |
|
3641
|
@ <label><input type="checkbox" name="aa" %s(aack)> \ |
|
3642
|
@ All "announcement" subscribers</label> \ |
|
3643
|
@ <a href="%R/subscribers?only=a" target="_blank">(list)</a><br> |
|
3644
|
@ <label><input type="checkbox" name="all" %s(allck)> \ |
|
3645
|
@ All subscribers</label> \ |
|
3646
|
@ <a href="%R/subscribers" target="_blank">(list)</a><br> |
|
3647
|
@ <label><input type="checkbox" name="mods" %s(modck)> \ |
|
3648
|
@ All moderators</label> \ |
|
3649
|
@ <a href="%R/setup_ulist?with=5" target="_blank">(list)</a><br></td> |
|
3650
|
@ </tr> |
|
3651
|
} |
|
3652
|
@ <tr> |
|
3653
|
@ <td class="form_label">Subject:</td> |
|
3654
|
@ <td><input type="text" name="subject" value="%h(PT("subject"))"\ |
|
3655
|
@ size="80"></td> |
|
3656
|
@ </tr> |
|
3657
|
@ <tr> |
|
3658
|
@ <td class="form_label">Message:</td> |
|
3659
|
@ <td><textarea name="msg" cols="80" rows="10" wrap="virtual">\ |
|
3660
|
@ %h(PT("msg"))</textarea> |
|
3661
|
@ </tr> |
|
3662
|
@ <tr> |
|
3663
|
@ <td></td> |
|
3664
|
if( fossil_strcmp(zName,"test2")==0 ){ |
|
3665
|
@ <td><input type="submit" name="submit" value="Dry Run"> |
|
3666
|
}else{ |
|
3667
|
@ <td><input type="submit" name="submit" value="Send Message"> |
|
3668
|
} |
|
3669
|
@ </tr> |
|
3670
|
@ </table> |
|
3671
|
@ </form> |
|
3672
|
if( g.perm.Setup ){ |
|
3673
|
@ <hr> |
|
3674
|
@ <p>Trouble-shooting Options:</p> |
|
3675
|
@ <ol> |
|
3676
|
@ <li> <a href="%R/announce">Normal Processing</a> |
|
3677
|
@ <li> Only <a href="%R/announce/test1">show POST parameters</a> |
|
3678
|
@ - Do not send the announcement. |
|
3679
|
@ <li> <a href="%R/announce/test2">Show the email text</a> but do |
|
3680
|
@ not actually send it. |
|
3681
|
@ <li> Send the message and also <a href="%R/announce/test3">show the |
|
3682
|
@ SMTP traffic</a> when using "relay" mode. |
|
3683
|
@ </ol> |
|
3684
|
} |
|
3685
|
style_finish_page(); |
|
3686
|
} |
|
3687
|
|