/* $Id$ */
/*
* Copyright (c) 2006 Nicholas Marriott <nicholas.marriott@gmail.com>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER
* IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
* OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "fdm.h"
#include "fetch.h"
void fetch_nntp_fill(struct account *, struct iolist *);
int fetch_nntp_commit(struct account *, struct mail *);
void fetch_nntp_abort(struct account *);
u_int fetch_nntp_total(struct account *);
void fetch_nntp_desc(struct account *, char *, size_t);
int fetch_nntp_code(char *);
int fetch_nntp_check(
struct account *, struct fetch_ctx *, char **, int *, u_int, ...);
int fetch_nntp_parse223(char *, u_int *, char **);
int fetch_nntp_load(struct account *);
int fetch_nntp_save(struct account *);
int fetch_nntp_bad(struct account *, const char *);
int fetch_nntp_invalid(struct account *, const char *);
int fetch_nntp_state_connect(struct account *, struct fetch_ctx *);
int fetch_nntp_state_connected(struct account *, struct fetch_ctx *);
int fetch_nntp_state_auth1(struct account *, struct fetch_ctx *);
int fetch_nntp_state_auth2(struct account *, struct fetch_ctx *);
int fetch_nntp_state_switch(struct account *, struct fetch_ctx *);
int fetch_nntp_state_group(struct account *, struct fetch_ctx *);
int fetch_nntp_state_reset(struct account *, struct fetch_ctx *);
int fetch_nntp_state_stat(struct account *, struct fetch_ctx *);
int fetch_nntp_state_wait(struct account *, struct fetch_ctx *);
int fetch_nntp_state_next(struct account *, struct fetch_ctx *);
int fetch_nntp_state_article(struct account *, struct fetch_ctx *);
int fetch_nntp_state_line(struct account *, struct fetch_ctx *);
int fetch_nntp_state_quit(struct account *, struct fetch_ctx *);
struct fetch fetch_nntp = {
"nntp",
fetch_nntp_state_connect,
fetch_nntp_fill,
fetch_nntp_commit,
fetch_nntp_abort,
NULL,
fetch_nntp_desc
};
int
fetch_nntp_bad(struct account *a, const char *line)
{
log_warnx("%s: unexpected data: %s", a->name, line);
return (FETCH_ERROR);
}
int
fetch_nntp_invalid(struct account *a, const char *line)
{
log_warnx("%s: invalid response: %s", a->name, line);
return (FETCH_ERROR);
}
/* Extract code from line. */
int
fetch_nntp_code(char *line)
{
char ch;
const char *errstr;
int n;
size_t len;
len = strspn(line, "0123456789");
if (len == 0)
return (-1);
ch = line[len];
line[len] = '\0';
n = strtonum(line, 100, 999, &errstr);
line[len] = ch;
if (errstr != NULL)
return (-1);
return (n);
}
/*
* Get line from server and check against list of codes. Returns -1 on error,
* 0 on success, a NULL line when out of data.
*/
int
fetch_nntp_check(struct account *a,
struct fetch_ctx *fctx, char **line, int *codep, u_int n, ...)
{
struct fetch_nntp_data *data = a->data;
va_list ap;
u_int i;
int code;
if (codep == NULL)
codep = &code;
do {
*line = io_readline2(data->io, &fctx->lbuf, &fctx->llen);
if (*line == NULL)
return (0);
*codep = fetch_nntp_code(*line);
if (*codep == -1)
goto error;
} while (*codep >= 100 && *codep <= 199);
va_start(ap, n);
for (i = n; i > 0; i--) {
if (*codep == va_arg(ap, int))
break;
}
va_end(ap);
if (i == 0)
goto error;
return (0);
error:
log_warnx("%s: unexpected data: %s", a->name, *line);
return (-1);
}
/* Extract id from 223 code. */
int
fetch_nntp_parse223(char *line, u_int *n, char **id)
{
char *ptr, *ptr2;
if (sscanf(line, "223 %u ", n) != 1)
return (-1);
ptr = strchr(line, '<');
if (ptr == NULL)
return (-1);
ptr2 = strchr(ptr, '>');
if (ptr2 == NULL)
return (-1);
ptr++;
*id = xmalloc(ptr2 - ptr + 1);
memcpy(*id, ptr, ptr2 - ptr);
(*id)[ptr2 - ptr] = '\0';
return (0);
}
/* Load NNTP cache file. */
int
fetch_nntp_load(struct account *a)
{
struct fetch_nntp_data *data = a->data;
struct fetch_nntp_group *group;
int fd;
FILE *f;
char *name, *id;
size_t namelen, idlen;
u_int last, i;
f = NULL;
if ((fd = openlock(data->path, O_RDONLY, conf.lock_types)) == -1) {
if (errno == ENOENT)
return (0);
log_warn("%s: %s", a->name, data->path);
goto error;
}
if ((f = fdopen(fd, "r")) == NULL) {
log_warn("%s: %s", a->name, data->path);
goto error;
}
for (;;) {
if (fscanf(f, "%zu ", &namelen) != 1) {
/* EOF is allowed only at the start of a line. */
if (feof(f))
break;
goto invalid;
}
name = xmalloc(namelen + 1);
if (fread(name, namelen, 1, f) != 1)
goto invalid;
name[namelen] = '\0';
if (fscanf(f, " %u ", &last) != 1)
goto invalid;
if (fscanf(f, "%zu ", &idlen) != 1)
goto invalid;
id = xmalloc(idlen + 1);
if (fread(id, idlen, 1, f) != 1)
goto invalid;
id[idlen] = '\0';
/* Got a group. Fill it in. */
group = NULL;
for (i = 0; i < ARRAY_LENGTH(&data->groups); i++) {
group = ARRAY_ITEM(&data->groups, i);
if (strcmp(group->name, name) == 0)
break;
}
if (i == ARRAY_LENGTH(&data->groups)) {
/*
* Not found. add it so it is saved when the file is
* resaved, but with ignore set so it isn't fetched.
*/
group = xcalloc(1, sizeof *group);
ARRAY_ADD(&data->groups, group);
group->ignore = 1;
group->name = xstrdup(name);
}
log_debug2("%s: found group in cache: %s", a->name, name);
group->last = last;
group->id = id;
xfree(name);
}
fclose(f);
closelock(fd, data->path, conf.lock_types);
return (0);
invalid:
log_warnx("%s: invalid cache entry", a->name);
error:
if (f != NULL)
fclose(f);
if (fd != -1)
closelock(fd, data->path, conf.lock_types);
return (-1);
}
/* Save NNTP cache file. */
int
fetch_nntp_save(struct account *a)
{
struct fetch_nntp_data *data = a->data;
struct fetch_nntp_group *group;
char *path = NULL, tmp[MAXPATHLEN];
int fd = -1;
FILE *f = NULL;
u_int i;
if (ppath(tmp, sizeof tmp, "%s.XXXXXXXXXX", data->path) != 0)
goto error;
if ((fd = mkstemp(tmp)) == -1)
goto error;
path = tmp;
cleanup_register(path);
if ((f = fdopen(fd, "r+")) == NULL)
goto error;
fd = -1;
for (i = 0; i < ARRAY_LENGTH(&data->groups); i++) {
group = ARRAY_ITEM(&data->groups, i);
if (group->id == NULL)
continue;
fprintf(f, "%zu %s %u %zu %s\n", strlen(group->name),
group->name, group->last, strlen(group->id), group->id);
}
if (fflush(f) != 0)
goto error;
if (fsync(fileno(f)) != 0)
goto error;
fclose(f);
f = NULL;
if (rename(path, data->path) == -1)
goto error;
cleanup_deregister(path);
return (0);
error:
log_warn("%s: %s", a->name, data->path);
if (f != NULL)
fclose(f);
if (fd != -1)
close(fd);
if (path != NULL) {
if (unlink(tmp) != 0)
fatal("unlink failed");
cleanup_deregister(path);
}
return (-1);
}
/* Fill io list. */
void
fetch_nntp_fill(struct account *a, struct iolist *iol)
{
struct fetch_nntp_data *data = a->data;
if (data->io != NULL)
ARRAY_ADD(iol, data->io);
}
/* Commit mail. We just do a save here. */
int
fetch_nntp_commit(struct account *a, unused struct mail *m)
{
if (fetch_nntp_save(a) != 0)
return (FETCH_ERROR);
return (FETCH_AGAIN);
}
/* Abort fetch and free everything. */
void
fetch_nntp_abort(struct account *a)
{
struct fetch_nntp_data *data = a->data;
struct fetch_nntp_group *group;
u_int i;
if (data->io != NULL) {
io_close(data->io);
io_free(data->io);
data->io = NULL;
}
for (i = 0; i < ARRAY_LENGTH(&data->groups); i++) {
group = ARRAY_ITEM(&data->groups, i);
xfree(group->name);
if (group->id != NULL)
xfree(group->id);
xfree(group);
}
ARRAY_FREE(&data->groups);
}
/* Connect to NNTP server. */
int
fetch_nntp_state_connect(struct account *a, struct fetch_ctx *fctx)
{
struct fetch_nntp_data *data = a->data;
struct fetch_nntp_group *group;
u_int i;
char *cause;
/* Initialise and load groups array. */
ARRAY_INIT(&data->groups);
for (i = 0; i < ARRAY_LENGTH(data->names); i++) {
group = xcalloc(1, sizeof *group);
group->name = xstrdup(ARRAY_ITEM(data->names, i));
group->id = NULL;
group->ignore = 0;
ARRAY_ADD(&data->groups, group);
}
if (fetch_nntp_load(a) != 0)
return (FETCH_ERROR);
/* Find the first active group, if any. */
data->group = 0;
while (ARRAY_ITEM(&data->groups, data->group)->ignore) {
data->group++;
if (data->group == ARRAY_LENGTH(&data->groups)) {
log_warnx("%s: no groups found", a->name);
return (FETCH_ERROR);
}
}
/* Connect to the server. */
data->io = connectproxy(&data->server,
conf.verify_certs, conf.proxy, IO_CRLF, conf.timeout, &cause);
if (data->io == NULL) {
log_warnx("%s: %s", a->name, cause);
xfree(cause);
return (FETCH_ERROR);
}
if (conf.debug > 3 && !conf.syslog)
data->io->dup_fd = STDOUT_FILENO;
fctx->state = fetch_nntp_state_connected;
return (FETCH_BLOCK);
}
/* Connected state. */
int
fetch_nntp_state_connected(struct account *a, struct fetch_ctx *fctx)
{
struct fetch_nntp_data *data = a->data;
char *line;
if (fetch_nntp_check(a, fctx, &line, NULL, 2, 200, 201) != 0)
return (FETCH_ERROR);
if (line == NULL)
return (FETCH_BLOCK);
if (data->user == NULL || data->pass == NULL) {
fctx->state = fetch_nntp_state_group;
return (FETCH_AGAIN);
}
io_writeline(data->io, "AUTHINFO USER %s", data->user);
fctx->state = fetch_nntp_state_auth1;
return (FETCH_BLOCK);
}
/* First authentication state. */
int
fetch_nntp_state_auth1(struct account *a, struct fetch_ctx *fctx)
{
struct fetch_nntp_data *data = a->data;
char *line;
if (fetch_nntp_check(a, fctx, &line, NULL, 1, 381) != 0)
return (FETCH_ERROR);
if (line == NULL)
return (FETCH_BLOCK);
io_writeline(data->io, "AUTHINFO PASS %s", data->pass);
fctx->state = fetch_nntp_state_auth2;
return (FETCH_BLOCK);
}
/* Second authentication state. */
int
fetch_nntp_state_auth2(struct account *a, struct fetch_ctx *fctx)
{
char *line;
if (fetch_nntp_check(a, fctx, &line, NULL, 2, 281) != 0)
return (FETCH_ERROR);
if (line == NULL)
return (FETCH_BLOCK);
fctx->state = fetch_nntp_state_group;
return (FETCH_AGAIN);
}
/*
* Switch to the next group. Missed out the first time since connect already
* finds the first group.
*/
int
fetch_nntp_state_switch(struct account *a, struct fetch_ctx *fctx)
{
struct fetch_nntp_data *data = a->data;
/* Find the next group. */
do {
data->group++;
if (data->group == ARRAY_LENGTH(&data->groups)) {
io_writeline(data->io, "QUIT");
fctx->state = fetch_nntp_state_quit;
return (FETCH_BLOCK);
}
} while (ARRAY_ITEM(&data->groups, data->group)->ignore);
fctx->state = fetch_nntp_state_group;
return (FETCH_AGAIN);
}
/* Group state. */
int
fetch_nntp_state_group(struct account *a, struct fetch_ctx *fctx)
{
struct fetch_nntp_data *data = a->data;
struct fetch_nntp_group *group;
group = ARRAY_ITEM(&data->groups, data->group);
log_debug("%s: fetching group: %s", a->name, group->name);
io_writeline(data->io, "GROUP %s", group->name);
fctx->state = fetch_nntp_state_stat;
return (FETCH_BLOCK);
}
/* Reset state. */
int
fetch_nntp_state_reset(struct account *a, struct fetch_ctx *fctx)
{
struct fetch_nntp_data *data = a->data;
struct fetch_nntp_group *group;
group = ARRAY_ITEM(&data->groups, data->group);
log_warnx("%s: last message not found. resetting group", a->name);
if (group->id != NULL) {
xfree(group->id);
group->id = NULL;
}
group->last = 0;
fctx->state = fetch_nntp_state_group;
return (FETCH_AGAIN);
}
/* Stat state. */
int
fetch_nntp_state_stat(struct account *a, struct fetch_ctx *fctx)
{
struct fetch_nntp_data *data = a->data;
struct fetch_nntp_group *group;
char *line;
u_int n;
group = ARRAY_ITEM(&data->groups, data->group);
if (fetch_nntp_check(a, fctx, &line, NULL, 1, 211) != 0)
return (FETCH_ERROR);
if (line == NULL)
return (FETCH_BLOCK);
if (sscanf(line, "211 %u %*u %u", &group->size, &n) != 2)
return (fetch_nntp_invalid(a, line));
if (group->last > n) {
log_warnx("%s: "
"new last %u is less than old %u", a->name, n, group->last);
fctx->state = fetch_nntp_state_reset;
return (FETCH_AGAIN);
}
group->size = n - group->last;
if (group->last != 0) {
io_writeline(data->io, "STAT %u", group->last);
fctx->state = fetch_nntp_state_wait;
return (FETCH_BLOCK);
} else {
io_writeline(data->io, "NEXT");
fctx->state = fetch_nntp_state_next;
return (FETCH_BLOCK);
}
}
/* Wait state. Wait for and check STAT response. */
int
fetch_nntp_state_wait(struct account *a, struct fetch_ctx *fctx)
{
struct fetch_nntp_data *data = a->data;
struct fetch_nntp_group *group;
char *line, *id;
u_int n;
int code;
group = ARRAY_ITEM(&data->groups, data->group);
if (fetch_nntp_check(a, fctx, &line, &code, 2, 223, 423) != 0)
return (FETCH_ERROR);
if (line == NULL)
return (FETCH_BLOCK);
if (code == 223) {
if (fetch_nntp_parse223(line, &n, &id) != 0)
return (fetch_nntp_invalid(a, line));
if (n != group->last) {
log_warnx("%s: unexpected message number", a->name);
xfree(id);
return (FETCH_ERROR);
}
if (strcmp(id, group->id) != 0) {
xfree(id);
fctx->state = fetch_nntp_state_reset;
return (FETCH_AGAIN);
}
log_debug2(
"%s: last message found: %u %s", a->name, group->last, id);
xfree(id);
} else
log_warnx("%s: could not get last message", a->name);
io_writeline(data->io, "NEXT");
fctx->state = fetch_nntp_state_next;
return (FETCH_BLOCK);
}
/* Next state. Now we are fetching mail. */
int
fetch_nntp_state_next(struct account *a, struct fetch_ctx *fctx)
{
struct fetch_nntp_data *data = a->data;
struct fetch_nntp_group *group;
char *line, *id;
u_int n;
int code;
group = ARRAY_ITEM(&data->groups, data->group);
if (fetch_nntp_check(a, fctx, &line, &code, 3, 223, 420, 421) != 0)
return (FETCH_ERROR);
if (line == NULL)
return (FETCH_BLOCK);
if (code == 420 || code == 421) {
/* Finished this group. Switch to the next. */
fctx->state = fetch_nntp_state_switch;
return (FETCH_AGAIN);
}
/* 223 code. Save this as last article. */
if (fetch_nntp_parse223(line, &n, &id) != 0)
return (fetch_nntp_invalid(a, line));
group->last = n;
if (group->id != NULL)
xfree(group->id);
group->id = id;
io_writeline(data->io, "ARTICLE");
fctx->state = fetch_nntp_state_article;
return (FETCH_BLOCK);
}
/* Article state. */
int
fetch_nntp_state_article(struct account *a, struct fetch_ctx *fctx)
{
struct fetch_nntp_data *data = a->data;
struct fetch_nntp_group *group;
struct mail *m = fctx->mail;
char *line;
int code;
group = ARRAY_ITEM(&data->groups, data->group);
if (fetch_nntp_check(a, fctx, &line, &code, 3, 220, 423, 430) != 0)
return (FETCH_ERROR);
if (line == NULL)
return (FETCH_BLOCK);
if (code == 423 || code == 430) {
io_writeline(data->io, "NEXT");
fctx->state = fetch_nntp_state_next;
return (FETCH_BLOCK);
}
/* Open the mail. */
if (mail_open(m, IO_BLOCKSIZE) != 0) {
log_warn("%s: failed to create mail", a->name);
return (FETCH_ERROR);
}
m->size = 0;
/* Tag mail. */
default_tags(&m->tags, group->name);
add_tag(&m->tags, "group", "%s", group->name);
add_tag(&m->tags, "server", "%s", data->server.host);
add_tag(&m->tags, "port", "%s", data->server.port);
data->flushing = 0;
fctx->state = fetch_nntp_state_line;
return (FETCH_AGAIN);
}
/* Line state. */
int
fetch_nntp_state_line(struct account *a, struct fetch_ctx *fctx)
{
struct fetch_nntp_data *data = a->data;
struct mail *m = fctx->mail;
char *line;
for (;;) {
line = io_readline2(data->io, &fctx->lbuf, &fctx->llen);
if (line == NULL)
return (FETCH_BLOCK);
if (line[0] == '.') {
if (line[1] == '\0')
break;
line++;
}
if (data->flushing)
continue;
if (append_line(m, line, strlen(line)) != 0) {
log_warn("%s: failed to resize mail", a->name);
return (FETCH_ERROR);
}
if (m->size > conf.max_size)
data->flushing = 1;
}
io_writeline(data->io, "NEXT");
fctx->state = fetch_nntp_state_next;
return (FETCH_MAIL);
}
/* Quit state. */
int
fetch_nntp_state_quit(struct account *a, struct fetch_ctx *fctx)
{
char *line;
if (fetch_nntp_check(a, fctx, &line, NULL, 1, 205) != 0)
return (FETCH_ERROR);
if (line == NULL)
return (FETCH_BLOCK);
fetch_nntp_abort(a);
return (FETCH_EXIT);
}
void
fetch_nntp_desc(struct account *a, char *buf, size_t len)
{
struct fetch_nntp_data *data = a->data;
char *names;
names = fmt_strings("groups ", data->names);
xsnprintf(buf, len, "nntp server \"%s\" port %s %s cache \"%s\"",
data->server.host, data->server.port, names, data->path);
xfree(names);
}