From bb6ae3fb80fff36cd342d6f6bff3dfabd7dd243e Mon Sep 17 00:00:00 2001 Message-Id: From: Mark Wooding Date: Sat, 29 Dec 2007 18:54:16 +0000 Subject: [PATCH] Online registration now sends email. The confirmation URL doesn't work yet. The mail comes from mail_sender which MUST be configured. Organization: Straylight/Edgeware From: rjk@greenend.org.uk <> sendmail() sends mail via the SMTP server specified in the config file. The default is localhost. inputline() is extended to cope with CRLF-flavored protocols. local_hostname() returns the FQDN of the local host (assuming you didn't screw up your /etc/hosts). mime_encode_text() and mime_to_qp() prepare text/* messages for sending in mail. --- lib/Makefile.am | 2 + lib/configuration.c | 3 + lib/configuration.h | 6 ++ lib/hostname.c | 59 +++++++++++ lib/hostname.h | 35 ++++++ lib/inputline.c | 14 ++- lib/inputline.h | 3 + lib/mime.c | 91 ++++++++++++++++ lib/mime.h | 4 + lib/sendmail.c | 247 +++++++++++++++++++++++++++++++++++++++++++ lib/sendmail.h | 41 +++++++ lib/test.c | 10 ++ server/dcgi.c | 16 ++- templates/login.html | 2 +- 14 files changed, 529 insertions(+), 4 deletions(-) create mode 100644 lib/hostname.c create mode 100644 lib/hostname.h create mode 100644 lib/sendmail.c create mode 100644 lib/sendmail.h diff --git a/lib/Makefile.am b/lib/Makefile.am index 9b84937..d658f86 100644 --- a/lib/Makefile.am +++ b/lib/Makefile.am @@ -40,6 +40,7 @@ libdisorder_a_SOURCES=charset.c charset.h \ hash.c hash.h \ heap.h \ hex.c hex.h \ + hostname.c hostname.h \ ifreq.c ifreq.h \ inputline.c inputline.h \ kvp.c kvp.h \ @@ -56,6 +57,7 @@ libdisorder_a_SOURCES=charset.c charset.h \ rights.c queue-rights.c rights.h \ rtp.h \ selection.c selection.h \ + sendmail.c sendmail.h \ signame.c signame.h \ sink.c sink.h \ speaker-protocol.c speaker-protocol.h \ diff --git a/lib/configuration.c b/lib/configuration.c index bb30c2f..e5dd120 100644 --- a/lib/configuration.c +++ b/lib/configuration.c @@ -928,6 +928,7 @@ static const struct conf conf[] = { { C(home), &type_string, validate_isabspath }, { C(listen), &type_stringlist, validate_port }, { C(lock), &type_boolean, validate_any }, + { C(mail_sender), &type_string, validate_any }, { C(mixer), &type_string, validate_ischr }, { C(multicast_loop), &type_boolean, validate_any }, { C(multicast_ttl), &type_integer, validate_non_negative }, @@ -948,6 +949,7 @@ static const struct conf conf[] = { { C(scratch), &type_string_accum, validate_isreg }, { C(short_display), &type_integer, validate_positive }, { C(signal), &type_signal, validate_any }, + { C(smtp_server), &type_string, validate_any }, { C(sox_generation), &type_integer, validate_non_negative }, { C(speaker_backend), &type_backend, validate_any }, { C(speaker_command), &type_string, validate_any }, @@ -1147,6 +1149,7 @@ static struct config *config_default(void) { c->dbversion = 2; c->cookie_login_lifetime = 86400; c->cookie_key_lifetime = 86400 * 7; + c->smtp_server = xstrdup("127.0.0.1"); if(config_set(&cs, (int)NDEFAULT_STOPWORDS, (char **)default_stopwords)) exit(1); return c; diff --git a/lib/configuration.h b/lib/configuration.h index abb95a6..d212a7a 100644 --- a/lib/configuration.h +++ b/lib/configuration.h @@ -255,6 +255,12 @@ struct config { /** @brief Default rights for a new user */ char *default_rights; + + /** @brief SMTP server for sending mail */ + char *smtp_server; + + /** @brief Origin address for outbound mail */ + char *mail_sender; /* derived values: */ int nparts; /* number of distinct name parts */ diff --git a/lib/hostname.c b/lib/hostname.c new file mode 100644 index 0000000..536bb8e --- /dev/null +++ b/lib/hostname.c @@ -0,0 +1,59 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2007 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include +#include "types.h" + +#include +#include +#include + +#include "mem.h" +#include "hostname.h" +#include "log.h" + +static const char *hostname; + +/** @brief Return the local hostname + * @return Hostname + */ +const char *local_hostname(void) { + if(!hostname) { + struct utsname u; + struct hostent *he; + + if(uname(&u) < 0) + fatal(errno, "error calling uname"); + if(!(he = gethostbyname(u.nodename))) + fatal(0, "cannot resolve '%s'", u.nodename); + hostname = xstrdup(he->h_name); + } + return hostname; +} + + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +indent-tabs-mode:nil +End: +*/ diff --git a/lib/hostname.h b/lib/hostname.h new file mode 100644 index 0000000..89bfeaa --- /dev/null +++ b/lib/hostname.h @@ -0,0 +1,35 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2007 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#ifndef HOSTNAME_H +#define HOSTNAME_H + +const char *local_hostname(void); + +#endif /* HOSTNAME_H */ + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +indent-tabs-mode:nil +End: +*/ diff --git a/lib/inputline.c b/lib/inputline.c index 3202eff..855910f 100644 --- a/lib/inputline.c +++ b/lib/inputline.c @@ -38,12 +38,16 @@ * @param tag Used in error messages * @param fp Stream to read from * @param lp Where to store newly allocated string - * @param newline Newline character + * @param newline Newline character or @ref CRLF * @return 0 on success, -1 on error or eof. * * The newline is not included in the string. If the last line of a * stream does not have a newline then that line is still returned. * + * If @p newline is @ref CRLF then the line is terminated by CR LF, + * not by a single newline character. The CRLF is still not included + * in the string in this case. + * * @p *lp is only set if the return value was 0. */ int inputline(const char *tag, FILE *fp, char **lp, int newline) { @@ -52,8 +56,14 @@ int inputline(const char *tag, FILE *fp, char **lp, int newline) { dynstr_init(&d); while((ch = getc(fp)), - (!ferror(fp) && !feof(fp) && ch != newline)) + (!ferror(fp) && !feof(fp) && ch != newline)) { dynstr_append(&d, ch); + if(newline == CRLF && d.nvec >= 2 + && d.vec[d.nvec - 2] == 0x0D && d.vec[d.nvec - 1] == 0x0A) { + d.nvec -= 2; + break; + } + } if(ferror(fp)) { error(errno, "error reading %s", tag); return -1; diff --git a/lib/inputline.h b/lib/inputline.h index 57d3975..937b767 100644 --- a/lib/inputline.h +++ b/lib/inputline.h @@ -26,6 +26,9 @@ int inputline(const char *tag, FILE *fp, char **lp, int newline); * them (excluding @newline@) via @lp@. Return 0 on success, -1 on * error/eof. */ +/** @brief Magic @p newline value to make inputline() insist on CRLF */ +#define CRLF 0x100 + #endif /* INPUTLINE_H */ /* diff --git a/lib/mime.c b/lib/mime.c index 9883a3c..422e1f5 100644 --- a/lib/mime.c +++ b/lib/mime.c @@ -566,6 +566,97 @@ char *quote822(const char *s, int force) { return d->vec; } +/** @brief Return true if @p ptr points at trailing space */ +static int is_trailing_space(const char *ptr) { + if(*ptr == ' ' || *ptr == '\t') { + while(*ptr == ' ' || *ptr == '\t') + ++ptr; + return *ptr == '\n' || *ptr == 0; + } else + return 0; +} + +/** @brief Encoding text as quoted-printable + * @param text String to encode + * @return Encoded string + * + * See RFC2045 + * s6.7. + */ +char *mime_to_qp(const char *text) { + struct dynstr d[1]; + int linelength = 0; /* length of current line */ + char buffer[10]; + + dynstr_init(d); + /* The rules are: + * 1. Anything except newline can be replaced with =%02X + * 2. Newline, 33-60 and 62-126 stand for themselves (i.e. not '=') + * 3. Non-trailing space/tab stand for themselves. + * 4. Output lines are limited to 76 chars, with = being used + * as a soft line break + * 5. Newlines aren't counted towards the 76 char limit. + */ + while(*text) { + const int c = (unsigned char)*text; + if(c == '\n') { + /* Newline stands as itself */ + dynstr_append(d, '\n'); + linelength = 0; + } else if((c >= 33 && c <= 126 && c != '=') + || ((c == ' ' || c == '\t') + && !is_trailing_space(text))) { + /* Things that can stand for themselves */ + dynstr_append(d, c); + ++linelength; + } else { + /* Anything else that needs encoding */ + snprintf(buffer, sizeof buffer, "=%02X", c); + dynstr_append_string(d, buffer); + linelength += 3; + } + ++text; + if(linelength > 73 && *text && *text != '\n') { + /* Next character might overflow 76 character limit if encoded, so we + * insert a soft break */ + dynstr_append_string(d, "=\n"); + linelength = 0; + } + } + /* Ensure there is a final newline */ + if(linelength) + dynstr_append(d, '\n'); + /* That's all */ + dynstr_terminate(d); + return d->vec; +} + +/** @brief Encode text + * @param text Underlying UTF-8 text + * @param charsetp Where to store charset string + * @param encodingp Where to store encoding string + * @return Encoded text (might be @ref text) + */ +const char *mime_encode_text(const char *text, + const char **charsetp, + const char **encodingp) { + const char *ptr; + + /* See if there are in fact any non-ASCII characters */ + for(ptr = text; *ptr; ++ptr) + if((unsigned char)*ptr >= 128) + break; + if(!*ptr) { + /* Plain old ASCII, no encoding required */ + *charsetp = "us-ascii"; + *encodingp = "7bit"; + return text; + } + *charsetp = "utf-8"; + *encodingp = "quoted-printable"; + return mime_to_qp(text); +} + /* Local Variables: c-basic-offset:2 diff --git a/lib/mime.h b/lib/mime.h index 0fa8aea..b863f4a 100644 --- a/lib/mime.h +++ b/lib/mime.h @@ -95,6 +95,10 @@ int parse_cookie(const char *s, const struct cookie *find_cookie(const struct cookiedata *cd, const char *name); char *quote822(const char *s, int force); +char *mime_to_qp(const char *text); +const char *mime_encode_text(const char *text, + const char **charsetp, + const char **encodingp); #endif /* MIME_H */ diff --git a/lib/sendmail.c b/lib/sendmail.c new file mode 100644 index 0000000..3ecfc40 --- /dev/null +++ b/lib/sendmail.c @@ -0,0 +1,247 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2007 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include +#include "types.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "syscalls.h" +#include "log.h" +#include "configuration.h" +#include "inputline.h" +#include "addr.h" +#include "hostname.h" +#include "sendmail.h" +#include "base64.h" + +/** @brief Get a server response + * @param tag Server name + * @param in Input stream + * @return Response code 0-999 or -1 on error + */ +static int getresponse(const char *tag, FILE *in) { + char *line; + + while(!inputline(tag, in, &line, CRLF)) { + if(line[0] >= '0' && line[0] <= '9' + && line[1] >= '0' && line[1] <= '9' + && line[2] >= '0' && line[2] <= '9') { + const int rc = 10 * (10 * line[0] + line[1]) + line[2] - 111 * '0'; + if(rc >= 400 && rc <= 599) + error(0, "%s: %s", tag, line); + if(line[3] != '-') { + return rc; + } + /* else go round for further response lines */ + } else { + error(0, "%s: malformed response: %s", tag, line); + return -1; + } + } + if(ferror(in)) + error(errno, "%s: read error", tag); + else + error(0, "%s: server closed connection", tag); + return -1; +} + +/** @brief Send a command to the server + * @param tag Server name + * @param out stream to send commands to + * @param fmt Format string + * @return 0 on success, non-0 on error + */ +static int sendcommand(const char *tag, FILE *out, const char *fmt, ...) { + va_list ap; + int rc; + + va_start(ap, fmt); + rc = vfprintf(out, fmt, ap); + va_end(ap); + if(rc >= 0) + rc = fputs("\r\n", out); + if(rc >= 0) + rc = fflush(out); + if(rc >= 0) + return 0; + error(errno, "%s: write error", tag); + return -1; +} + +/** @brief Send a mail message + * @param tag Server name + * @param in Stream to read responses from + * @param out Stream to send commands to + * @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 body_type Content-type of body + * @param body Text of body (encoded, but \n for newline) + * @return 0 on success, non-0 on error + */ +static int sendmailfp(const char *tag, FILE *in, FILE *out, + const char *sender, + const char *pubsender, + const char *recipient, + const char *subject, + const char *encoding, + const char *content_type, + const char *body) { + int rc, sol = 1; + const char *ptr; + uint8_t idbuf[20]; + char *id; + + gcry_create_nonce(idbuf, sizeof idbuf); + id = mime_to_base64(idbuf, sizeof idbuf); + if((rc = getresponse(tag, in)) / 100 != 2) + return -1; + if(sendcommand(tag, out, "HELO %s", local_hostname())) + return -1; + if((rc = getresponse(tag, in)) / 100 != 2) + return -1; + if(sendcommand(tag, out, "MAIL FROM:<%s>", sender)) + return -1; + if((rc = getresponse(tag, in)) / 100 != 2) + return -1; + if(sendcommand(tag, out, "RCPT TO:<%s>", recipient)) + return -1; + if((rc = getresponse(tag, in)) / 100 != 2) + return -1; + if(sendcommand(tag, out, "DATA", sender)) + return -1; + if((rc = getresponse(tag, in)) / 100 != 3) + return -1; + if(fprintf(out, "From: %s\r\n", pubsender) < 0 + || fprintf(out, "To: %s\r\n", recipient) < 0 + || fprintf(out, "Subject: %s\r\n", subject) < 0 + || fprintf(out, "Message-ID: <%s@%s>\r\n", id, local_hostname()) < 0 + || fprintf(out, "MIME-Version: 1.0\r\n") < 0 + || fprintf(out, "Content-Type: %s\r\n", content_type) < 0 + || fprintf(out, "Content-Transfer-Encoding: %s\r\n", encoding) < 0 + || fprintf(out, "\r\n") < 0) { + write_error: + error(errno, "%s: write error", tag); + return -1; + } + for(ptr = body; *ptr; ++ptr) { + if(sol && *ptr == '.') + if(fputc('.', out) < 0) + goto write_error; + if(*ptr == '\n') { + if(fputc('\r', out) < 0) + goto write_error; + sol = 1; + } else + sol = 0; + if(fputc(*ptr, out) < 0) + goto write_error; + } + if(!sol) + if(fputs("\r\n", out) < 0) + goto write_error; + if(fprintf(out, ".\r\n") < 0 + || fflush(out) < 0) + goto write_error; + if((rc = getresponse(tag, in)) / 100 != 2) + return -1; + return 0; +} + +/** @brief 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 0 on success, non-0 on error + * + * See mime_encode_text() for encoding of text bodies. + */ +int sendmail(const char *sender, + const char *pubsender, + const char *recipient, + const char *subject, + const char *encoding, + const char *content_type, + const char *body) { + struct stringlist a; + char *s[2]; + struct addrinfo *ai; + char *tag; + int fdin, fdout, rc; + FILE *in, *out; + + static const struct addrinfo pref = { + 0, + PF_INET, + SOCK_STREAM, + IPPROTO_TCP, + 0, + 0, + 0, + 0 + }; + + /* Find the SMTP server */ + a.n = 2; + a.s = s; + s[0] = config->smtp_server; + s[1] = (char *)"smtp"; + if(!(ai = get_address(&a, &pref, &tag))) + return -1; + fdin = xsocket(ai->ai_family, ai->ai_socktype, ai->ai_protocol); + if(connect(fdin, ai->ai_addr, ai->ai_addrlen) < 0) { + error(errno, "error connecting to %s", tag); + xclose(fdin); + return -1; + } + if((fdout = dup(fdin)) < 0) + fatal(errno, "error calling dup2"); + if(!(in = fdopen(fdin, "rb"))) + fatal(errno, "error calling fdopen"); + if(!(out = fdopen(fdout, "wb"))) + fatal(errno, "error calling fdopen"); + rc = sendmailfp(tag, in, out, sender, pubsender, recipient, subject, + encoding, content_type, body); + fclose(in); + fclose(out); + return rc; +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +indent-tabs-mode:nil +End: +*/ diff --git a/lib/sendmail.h b/lib/sendmail.h new file mode 100644 index 0000000..7849ef6 --- /dev/null +++ b/lib/sendmail.h @@ -0,0 +1,41 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2007 Richard Kettlewell + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#ifndef SENDMAIL_H +#define SENDMAIL_H + +int sendmail(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 */ + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +indent-tabs-mode:nil +End: +*/ diff --git a/lib/test.c b/lib/test.c index 61b63fd..27651ec 100644 --- a/lib/test.c +++ b/lib/test.c @@ -454,6 +454,16 @@ static void test_mime(void) { check_string(mime_qp("x =\r\ny"), "x y"); check_string(mime_qp("x = \r\ny"), "x y"); + check_string(mime_to_qp(""), ""); + check_string(mime_to_qp("foobar\n"), "foobar\n"); + check_string(mime_to_qp("foobar \n"), "foobar=20\n"); + check_string(mime_to_qp("foobar\t\n"), "foobar=09\n"); + check_string(mime_to_qp("foobar \t \n"), "foobar=20=09=20\n"); + check_string(mime_to_qp(" foo=bar"), " foo=3Dbar\n"); + check_string(mime_to_qp("copyright \xC2\xA9"), "copyright =C2=A9\n"); + check_string(mime_to_qp("foo\nbar\nbaz\n"), "foo\nbar\nbaz\n"); + check_string(mime_to_qp("wibble wobble wibble wobble wibble wobble wibble wobble wibble wobble wibble"), "wibble wobble wibble wobble wibble wobble wibble wobble wibble wobble wibb=\nle\n"); + /* from RFC2045 */ check_string(mime_qp("Now's the time =\r\n" "for all folk to come=\r\n" diff --git a/server/dcgi.c b/server/dcgi.c index a4f774a..d878330 100644 --- a/server/dcgi.c +++ b/server/dcgi.c @@ -56,6 +56,7 @@ #include "dcgi.h" #include "url.h" #include "mime.h" +#include "sendmail.h" char *login_cookie; @@ -497,7 +498,8 @@ static void act_logout(cgi_sink *output, static void act_register(cgi_sink *output, dcgi_state *ds) { const char *username, *password, *email; - char *confirm; + char *confirm, *content_type; + const char *text, *encoding, *charset; username = cgi_get("username"); password = cgi_get("password"); @@ -530,6 +532,18 @@ static void act_register(cgi_sink *output, expand_template(ds, output, "login"); return; } + /* Send the user a mail */ + /* TODO templatize this */ + byte_xasprintf((char **)&text, + "Welcome to DisOrder. To active your login, please visit this URL:\n" + "\n" + " %s?confirm=%s\n", config->url, confirm); + if(!(text = mime_encode_text(text, &charset, &encoding))) + fatal(0, "cannot encode email"); + byte_xasprintf(&content_type, "text/plain;charset=%s", + quote822(charset, 0)); + sendmail("", config->mail_sender, email, "Welcome to DisOrder", + encoding, content_type, text); /* TODO error checking */ /* We'll go back to the login page with a suitable message */ cgi_set_option("registered", "registeredok"); expand_template(ds, output, "login"); diff --git a/templates/login.html b/templates/login.html index 16811f8..6c1d0b9 100644 --- a/templates/login.html +++ b/templates/login.html @@ -95,7 +95,7 @@ USA @label:login.password@ -