The target size of the queue. If random play is enabled then randomly picked
tracks will be added until the queue is at least this big. The default is 10.
.TP
+.B reminder_interval \fISECONDS\fR
+The minimum number of seconds that must elapse between password reminders. The
+default is 600, i.e. 10 minutes.
+.TP
.B sample_format \fIBITS\fB/\fIRATE\fB/\fICHANNELS
Describes the sample format expected by the \fBspeaker_command\fR (below). The
components of the format specification are as follows:
confirmation string; the user will be be able to log in until this has been
presented back to the server via the \fBconfirm\fR command.
.TP
+.B reminder \fIUSER\fR
+Send a password reminder to \fIUSER\fR. If the user has no valid email
+address, or no password, or a reminder has been sent too recently, then no
+reminder will be sent.
+.TP
.B remove \fIID\fR
Remove the track identified by \fIID\fR. Requires one of the \fBremove
mine\fR, \fBremove random\fR or \fBremove any\fR rights depending on how the
return disorder_simple(c, 0, "revoke", (char *)0);
}
+/** @brief Request a password reminder email
+ * @param c Client
+ * @param user Username
+ * @return 0 on success, non-0 on error
+ */
+int disorder_reminder(disorder_client *c, const char *user) {
+ return disorder_simple(c, 0, "reminder", user, (char *)0);
+}
+
/*
Local Variables:
c-basic-offset:2
int disorder_make_cookie(disorder_client *c, char **cookiep);
const char *disorder_last(disorder_client *c);
int disorder_revoke(disorder_client *c);
+int disorder_reminder(disorder_client *c, const char *user);
#endif /* CLIENT_H */
{ C(prefsync), &type_integer, validate_positive },
{ C(queue_pad), &type_integer, validate_positive },
{ C(refresh), &type_integer, validate_positive },
+ { C(reminder_interval), &type_integer, validate_positive },
{ C2(restrict, restrictions), &type_restrict, validate_any },
{ C(sample_format), &type_sample_format, validate_sample_format },
{ C(scratch), &type_string_accum, validate_isreg },
c->cookie_key_lifetime = 86400 * 7;
c->smtp_server = xstrdup("127.0.0.1");
c->new_max = 100;
+ c->reminder_interval = 600; /* 10m */
/* Default stopwords */
if(config_set(&cs, (int)NDEFAULT_STOPWORDS, (char **)default_stopwords))
exit(1);
/** @brief Maximum number of tracks in response to 'new' */
long new_max;
+
+ /** @brief Minimum interval between password reminder emails */
+ long reminder_interval;
/* derived values: */
int nparts; /* number of distinct name parts */
return rc;
}
+/** @brief Start a subproces to send a mail message
+ * @param sender Sender address (can be "")
+ * @param pubsender Visible sender address (must not be "")
+ * @param recipient Recipient address
+ * @param subject Subject string
+ * @param encoding Body encoding
+ * @param content_type Content-type of body
+ * @param body Text of body (encoded, but \n for newline)
+ * @return Subprocess PID on success, -1 on error
+ */
+pid_t sendmail_subprocess(const char *sender,
+ const char *pubsender,
+ const char *recipient,
+ const char *subject,
+ const char *encoding,
+ const char *content_type,
+ const char *body) {
+ pid_t pid;
+
+ if(!(pid = fork())) {
+ exitfn = _exit;
+ if(sendmail(sender, pubsender, recipient, subject,
+ encoding, content_type, body))
+ _exit(1);
+ _exit(0);
+ }
+ if(pid < 0)
+ error(errno, "error calling fork");
+ return pid;
+}
+
/*
Local Variables:
c-basic-offset:2
const char *encoding,
const char *content_type,
const char *body);
+pid_t sendmail_subprocess(const char *sender,
+ const char *pubsender,
+ const char *recipient,
+ const char *subject,
+ const char *encoding,
+ const char *content_type,
+ const char *body);
#endif /* SENDMAIL_H */
expand_template(ds, output, "login");
}
+static void act_reminder(cgi_sink *output,
+ dcgi_state *ds) {
+ const char *const username = cgi_get("username");
+
+ if(!username || !*username) {
+ cgi_set_option("error", "nousername");
+ expand_template(ds, output, "login");
+ return;
+ }
+ if(disorder_reminder(ds->g->client, username)) {
+ cgi_set_option("error", "reminderfailed");
+ expand_template(ds, output, "login");
+ return;
+ }
+ cgi_set_option("status", "reminded");
+ expand_template(ds, output, "login");
+}
static const struct action {
const char *name;
{ "random-disable", act_random_disable },
{ "random-enable", act_random_enable },
{ "register", act_register },
+ { "reminder", act_reminder },
{ "remove", act_remove },
{ "resume", act_resume },
{ "scratch", act_scratch },
#include "unicode.h"
#include "cookies.h"
#include "base64.h"
+#include "hash.h"
+#include "mime.h"
+#include "sendmail.h"
+#include "wstat.h"
#ifndef NONCE_SIZE
# define NONCE_SIZE 16
}
return 1;
}
-
+
+static int sent_reminder(ev_source attribute((unused)) *ev,
+ pid_t attribute((unused)) pid,
+ int status,
+ const struct rusage attribute((unused)) *rusage,
+ void *u) {
+ struct conn *const c = u;
+
+ /* Tell the client what went down */
+ if(!status) {
+ sink_writes(ev_writer_sink(c->w), "250 OK\n");
+ } else {
+ error(0, "reminder subprocess %s", wstat(status));
+ sink_writes(ev_writer_sink(c->w), "550 Cannot send a reminder email\n");
+ }
+ /* Re-enable this connection */
+ ev_reader_enable(c->r);
+ return 0;
+}
+
+static int c_reminder(struct conn *c,
+ char **vec,
+ int attribute((unused)) nvec) {
+ struct kvp *k;
+ const char *password, *email, *text, *encoding, *charset, *content_type;
+ const time_t *last;
+ time_t now;
+ pid_t pid;
+
+ static hash *last_reminder;
+
+ if(!config->mail_sender) {
+ error(0, "cannot send password reminders because mail_sender not set");
+ sink_writes(ev_writer_sink(c->w), "550 Cannot send a reminder email\n");
+ return 1;
+ }
+ if(!(k = trackdb_getuserinfo(vec[0]))) {
+ error(0, "reminder for user '%s' who does not exist", vec[0]);
+ sink_writes(ev_writer_sink(c->w), "550 Cannot send a reminder email\n");
+ return 1;
+ }
+ if(!(email = kvp_get(k, "email"))
+ || !strchr(email, '@')) {
+ error(0, "user '%s' has no valid email address", vec[0]);
+ sink_writes(ev_writer_sink(c->w), "550 Cannot send a reminder email\n");
+ return 1;
+ }
+ if(!(password = kvp_get(k, "password"))
+ || !*password) {
+ error(0, "user '%s' has no password", vec[0]);
+ sink_writes(ev_writer_sink(c->w), "550 Cannot send a reminder email\n");
+ return 1;
+ }
+ /* Rate-limit reminders. This hash is bounded in size by the number of
+ * users. If this is actually a problem for anyone then we can periodically
+ * clean it. */
+ if(!last_reminder)
+ last_reminder = hash_new(sizeof (time_t));
+ last = hash_find(last_reminder, vec[0]);
+ time(&now);
+ if(last && now < *last + config->reminder_interval) {
+ error(0, "sent a password reminder to '%s' too recently", vec[0]);
+ sink_writes(ev_writer_sink(c->w), "550 Cannot send a reminder email\n");
+ return 1;
+ }
+ /* Send the reminder */
+ /* TODO this should be templatized and to some extent merged with
+ * the code in act_register() */
+ byte_xasprintf((char **)&text,
+"Someone requested that you be sent a reminder of your DisOrder password.\n"
+"Your password is:\n"
+"\n"
+" %s\n", password);
+ if(!(text = mime_encode_text(text, &charset, &encoding)))
+ fatal(0, "cannot encode email");
+ byte_xasprintf((char **)&content_type, "text/plain;charset=%s",
+ quote822(charset, 0));
+ pid = sendmail_subprocess("", config->mail_sender, email,
+ "DisOrder password reminder",
+ encoding, content_type, text);
+ if(pid < 0) {
+ sink_writes(ev_writer_sink(c->w), "550 Cannot send a reminder email\n");
+ return 1;
+ }
+ hash_add(last_reminder, vec[0], &now, HASH_INSERT_OR_REPLACE);
+ info("sending a passsword reminder to user '%s'", vec[0]);
+ /* We can only continue when the subprocess finishes */
+ ev_child(c->ev, pid, 0, sent_reminder, c);
+ return 0;
+}
+
static const struct command {
/** @brief Command name */
const char *name;
{ "recent", 0, 0, c_recent, RIGHT_READ },
{ "reconfigure", 0, 0, c_reconfigure, RIGHT_ADMIN },
{ "register", 3, 3, c_register, RIGHT_REGISTER|RIGHT__LOCAL },
+ { "reminder", 1, 1, c_reminder, RIGHT__LOCAL },
{ "remove", 1, 1, c_remove, RIGHT_REMOVE__MASK },
{ "rescan", 0, 0, c_rescan, RIGHT_RESCAN },
{ "resolve", 1, 1, c_resolve, RIGHT_READ },
background-color: #e0ffe0 /* pastel green */
}
+form.reminder {
+ border: 1px solid black;
+ background-color: #e0e0ff /* pastel blue */
+}
+
form.register {
border: 1px solid black;
background-color: #e0e0ff /* pastel blue */
</td>
</tr>
<tr>
- <td>
+ <td colspan=2>
<button class=login name=button type=submit>
@label:login.login@
</button>
<input name=back type=hidden value="@arg:back@">
</form>
+ <p>If you've forgotten your password, use this form to request an
+ email reminder. A reminder can only be sent if you registered with
+ your email address, and if a reminder has been sent too recently
+ then it won't be possible to send one.</p>
+
+ <form class=reminder action="@url@" method=POST
+ enctype="multipart/form-data" accept-charset=utf-8>
+ <table class=login>
+ <tr>
+ <td>@label:login.username@</td>
+ <td>
+ <input class=username name=username type=text size=32
+ value="@arg:username@">
+ </td>
+ </tr>
+ <tr>
+ <td colspan=2>
+ <button class=login name=button type=submit>
+ @label:login.reminder@
+ </button>
+ </td>
+ </tr>
+ </table>
+ <input name=action type=hidden value=reminder>
+ <input name=nonce type=hidden value="@nonce@">
+ </form>
+
@right{register}{
<h2>New Users</h2>
<td class=extra>@label:login.registerpassword2extra@</td>
</tr>
<tr>
- <td>
+ <td colspan=3>
<button class=register name=button>
@label:login.register@
</button>
<td class=extra>@label:login.edituserpassword2extra@</td>
</tr>
<tr>
- <td>
+ <td colspan=3>
<button class=edituser name=submit type=submit>
@label:login.edituser@
</button>
label login.register "Register"
label login.edituser "Change Details"
label login.logout "Logout"
+label login.reminder "Send reminder"
# Text for login page responses
label login.loginok "You are now logged in."
label login.registered "Your new login has been registered. Please check your email."
label login.confirmed "Your new login has been confirmed. You are now logged in."
label login.edited "Your details have been changed."
+label login.reminded "You have been sent a reminder email."
# <TITLE> for account page
label account.title "DisOrder User Details"
label error.noconfirm "Missing confirmation string."
label error.badconfirm "Invalid confirmation string."
label error.badedit "Cannot edit user details."
+label error.reminderfailed "Cannot send a reminder."
# Text appended to all error pages
label error.generic ""
<a class=@if{@or{@eq{@action@}{login}@}
{@eq{@action@}{logout}@}
{@eq{@action@}{register}@}
+ {@eq{@action@}{reminder}@}
{@eq{@action@}{edituser}@}@}{activemenu}{inactivemenu}@
href="@url@?action=login&nonce=@nonce@"
title="@label:sidebar.loginverbose@">@label:sidebar.login@</a>