Fossil SCM

fossil-scm / src / alerts.c
Blame History Raw 3687 lines
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 &ensp; <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&nbsp;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'>&uarr; %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'>&uarr; %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'>&uarr; %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&nbsp;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'>&larr; not a valid email address!</span>
2070
}else if( g.perm.Admin ){
2071
@ &nbsp;&nbsp;<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&nbsp;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)) &larr; \
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
@ &nbsp;&nbsp;<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>&nbsp;&uarr;&nbsp;
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">&larr; %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>&larr;</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>&larr;</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&nbsp;Address:</td>
2394
@ <td><input type="text" name="e" value="%h(zEAddr)" size="30"></td>
2395
if( eErr==1 ){
2396
@ <td><span class="loginError">&larr; %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">&larr; %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&nbsp;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&nbsp;Email&nbsp;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

Keyboard Shortcuts

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