/* Copyright (C) 2020 C. McEnroe <june@causal.agency>
*
* 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 3 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, see <https://www.gnu.org/licenses/>.
*
* Additional permission under GNU GPL version 3 section 7:
*
* If you modify this Program, or any covered work, by linking or
* combining it with OpenSSL (or a modified version of that library),
* containing parts covered by the terms of the OpenSSL License and the
* original SSLeay license, the licensors of this Program grant you
* additional permission to convey the resulting work. Corresponding
* Source for a non-source form of such a combination shall include the
* source code for the parts of OpenSSL used as well as that of the
* covered work.
*/
#include <err.h>
#include <errno.h>
#include <inttypes.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <strings.h>
#include <sys/stat.h>
#include <sysexits.h>
#include <unistd.h>
#include "archive.h"
#include "imap.h"
static char *exportPath(uint32_t uid, const char *type) {
struct Variable vars[] = {
{ "uid", u32(uid).s },
{ "type", type },
{0},
};
return templateString(PATH_UID, vars, escapePath);
}
bool exportFetch(FILE *imap, enum Atom tag, struct List threads) {
struct List uids = {0};
listFlatten(&uids, threads);
for (size_t i = uids.len - 1; i < uids.len; --i) {
uint32_t uid = dataCheck(uids.ptr[i], Number).number;
char *atom = exportPath(uid, "atom");
char *html = exportPath(uid, "html");
char *mbox = exportPath(uid, "mbox");
int error = 0
|| access(atom, F_OK)
|| access(html, F_OK)
|| access(mbox, F_OK);
if (!error) uids.ptr[i] = uids.ptr[--uids.len];
free(atom);
free(html);
free(mbox);
}
if (!uids.len) {
listFree(uids);
return false;
}
fprintf(imap, "%s UID FETCH ", Atoms[tag]);
for (size_t i = 0; i < uids.len; ++i) {
fprintf(imap, "%s%" PRIu32, (i ? "," : ""), uids.ptr[i].number);
}
listFree(uids);
fprintf(
imap,
" (UID ENVELOPE BODYSTRUCTURE"
" BODY[HEADER.FIELDS (" MBOX_HEADERS ")] BODY[TEXT])\r\n"
);
return true;
}
static void exportMbox(
uint32_t uid, const struct Envelope *envelope,
const char *header, const char *body
) {
char *path = exportPath(uid, "mbox");
FILE *file = fopen(path, "w");
if (!file) err(EX_CANTCREAT, "%s", path);
int error = 0
|| mboxFrom(file)
|| mboxHeader(file, header)
|| mboxBody(file, body)
|| fclose(file);
if (error) err(EX_IOERR, "%s", path);
struct Variable vars[] = {
{ "messageID", envelope->messageID },
{ "type", "mbox" },
{0},
};
char *dest = templateString(PATH_MESSAGE, vars, escapePath);
unlink(dest);
error = link(path, dest);
if (error) err(EX_CANTCREAT, "%s", dest);
free(dest);
free(path);
}
static bool isInline(const struct BodyPart *part) {
if (!bodyPartType(part, "text", "plain")) return false;
if (!part->disposition.type) return true;
return !strcasecmp(part->disposition.type, "inline");
}
static bool isAttachment(const struct BodyPart *part) {
if (isInline(part)) return false;
return !part->multipart && !part->message.structure;
}
static void exportAtom(
uint32_t uid, const struct Envelope *envelope,
const struct BodyPart *structure, struct Data body
) {
char *path = exportPath(uid, "atom");
FILE *file = fopen(path, "w");
if (!file) err(EX_CANTCREAT, "%s", path);
int error = atomEntryOpen(file, envelope);
if (error) err(EX_IOERR, "%s", path);
const struct BodyPart *part = structure;
while (part->multipart) {
if (bodyPartType(part, "multipart", "alternative")) {
for (size_t i = part->parts.len - 1; i < part->parts.len; --i) {
if (!isInline(&part->parts.ptr[i])) continue;
part = &part->parts.ptr[i];
body = dataCheck(body, List).list.ptr[i];
break;
}
if (part->multipart) break;
} else {
part = &part->parts.ptr[0];
body = dataCheck(body, List).list.ptr[0];
}
}
if (isInline(part)) {
char *content = decodeToString(part, dataCheck(body, String).string);
error = atomContent(file, content);
if (error) err(EX_IOERR, "%s", path);
free(content);
}
error = atomEntryClose(file) || fclose(file);
if (error) err(EX_IOERR, "%s", path);
free(path);
}
static char *sectionSpec(struct List section) {
char *buf;
size_t len;
FILE *file = open_memstream(&buf, &len);
if (!file) err(EX_OSERR, "open_memstream");
for (size_t i = 0; i < section.len; ++i) {
fprintf(
file, "%s%" PRIu32,
(i ? "." : ""), dataCheck(section.ptr[i], Number).number
);
}
int error = fclose(file);
if (error) err(EX_OSERR, "open_memstream");
return buf;
}
static int exportHTMLAttachment(
FILE *file, const struct Envelope *envelope, struct List section,
const struct BodyPart *part, struct Data body
) {
const char *name = paramGet(part->disposition.params, "filename");
if (!name) name = paramGet(part->params, "name");
const char *disposition = part->disposition.type;
if (!disposition) disposition = "INLINE";
char *spec = sectionSpec(section);
struct Variable vars[] = {
{ "messageID", envelope->messageID },
{ "section", spec },
{ "name", (name ? name : "") },
{ "disposition", (name ? "" : disposition) },
{ ".", (name ? "" : ".") },
{ "subtype", (name ? "" : part->subtype) },
{0},
};
char *path = templateString(PATH_ATTACHMENT, vars, escapePath);
for (char *ch = path; (ch = strchr(ch, '/')); ++ch) {
*ch = '\0';
int error = mkdir(path, 0775);
if (error && errno != EEXIST) err(EX_CANTCREAT, "%s", path);
*ch = '/';
}
FILE *attachment = fopen(path, "w");
if (!file) err(EX_CANTCREAT, "%s", path);
int error = 0
|| decodeToFile(attachment, part, dataCheck(body, String).string)
|| fclose(attachment);
if (error) err(EX_IOERR, "%s", path);
free(path);
error = htmlAttachment(file, part, vars);
free(spec);
return error;
}
static int exportHTMLBody(
FILE *file, const struct Envelope *envelope, struct List *section,
const struct BodyPart *part, struct Data body
) {
if (bodyPartType(part, "multipart", "alternative")) {
for (size_t i = part->parts.len - 1; i < part->parts.len; --i) {
if (!isInline(&part->parts.ptr[i])) continue;
return exportHTMLBody(
file, envelope, section,
&part->parts.ptr[i], dataCheck(body, List).list.ptr[i]
);
}
}
if (part->multipart) {
int error;
bool attached = false;
for (size_t i = 0; i < part->parts.len; ++i) {
if (attached != isAttachment(&part->parts.ptr[i])) {
attached ^= true;
error = attached
? htmlAttachmentOpen(file)
: htmlAttachmentClose(file);
if (error) return error;
}
struct Data num = { .type = Number, .number = 1 + i };
listPush(section, num);
error = exportHTMLBody(
file, envelope, section,
&part->parts.ptr[i], dataCheck(body, List).list.ptr[i]
);
if (error) return error;
section->len--;
}
return (attached ? htmlAttachmentClose(file) : 0);
} else if (part->message.structure) {
const struct BodyPart *structure = part->message.structure;
int error = 0
|| htmlMessageOpen(file, part->message.envelope)
|| exportHTMLBody(file, envelope, section, structure, body)
|| htmlMessageClose(file);
return error;
} else if (isInline(part)) {
char *content = decodeToString(part, dataCheck(body, String).string);
int error = htmlInline(file, part, content);
free(content);
return error;
} else {
return exportHTMLAttachment(file, envelope, *section, part, body);
}
}
static void exportHTML(
uint32_t uid, const struct Envelope *envelope,
const struct BodyPart *structure, struct Data body
) {
char *path = exportPath(uid, "html");
FILE *file = fopen(path, "w");
if (!file) err(EX_CANTCREAT, "%s", path);
int error = htmlMessageOpen(file, envelope);
if (error) err(EX_IOERR, "%s", path);
struct List section = {0};
error = exportHTMLBody(file, envelope, §ion, structure, body);
if (error) err(EX_IOERR, "%s", path);
listFree(section);
error = htmlMessageClose(file) || fclose(file);
if (error) err(EX_IOERR, "%s", path);
free(path);
}
static void fetchParts(
FILE *imap, struct List *section, const struct BodyPart *structure
) {
if (structure->multipart) {
for (size_t i = 0; i < structure->parts.len; ++i) {
struct Data part = { .type = Number, .number = 1 + i };
listPush(section, part);
fetchParts(imap, section, &structure->parts.ptr[i]);
section->len--;
}
} else if (
structure->message.structure &&
structure->message.structure->multipart
) {
fetchParts(imap, section, structure->message.structure);
} else {
char *spec = sectionSpec(*section);
fprintf(
imap, " BODY[%s%s]",
spec, (structure->message.structure ? ".TEXT" : "")
);
free(spec);
}
}
static void checkBodyParts(const struct BodyPart *structure, struct Data body) {
if (structure->multipart) {
struct List list = dataCheck(body, List).list;
if (list.len < structure->parts.len) {
errx(EX_PROTOCOL, "missing body parts");
}
for (size_t i = 0; i < structure->parts.len; ++i) {
checkBodyParts(&structure->parts.ptr[i], list.ptr[i]);
}
} else if (
structure->message.structure &&
structure->message.structure->multipart
) {
checkBodyParts(structure->message.structure, body);
} else if (body.type != String) {
errx(EX_PROTOCOL, "missing body part");
}
}
bool exportData(FILE *imap, enum Atom tag, struct List items) {
uint32_t uid = 0;
struct Envelope envelope = {0};
struct BodyPart structure = {0};
struct Data bodyHeader = {0};
struct Data bodyText = {0};
struct Data bodyParts = {0};
for (size_t i = 0; i + 1 < items.len; i += 2) {
enum Atom name = dataCheck(items.ptr[i], Atom).atom;
struct Data data = items.ptr[i + 1];
if (name == AtomUID) {
uid = dataCheck(data, Number).number;
} else if (name == AtomEnvelope) {
parseEnvelope(&envelope, dataCheck(data, List).list);
} else if (name == AtomBodyStructure) {
parseBodyPart(&structure, dataCheck(data, List).list);
}
if (name != AtomBody) continue;
struct List section = dataCheck(data, List).list;
if (!section.len) {
errx(EX_PROTOCOL, "missing body data item section");
}
i++;
if (i + 1 >= items.len) {
errx(EX_PROTOCOL, "missing body data item value");
}
data = items.ptr[i + 1];
if (section.ptr[0].type == Atom) {
name = section.ptr[0].atom;
if (name == AtomHeader) {
bodyHeader = data;
} else if (name == AtomText) {
bodyText = data;
}
continue;
}
struct Data *dest = &bodyParts;
for (size_t i = 0; i < section.len; ++i) {
if (section.ptr[i].type != Number) continue;
uint32_t num = section.ptr[i].number;
if (dest->type != List) {
*dest = (struct Data) { .type = List };
}
while (dest->list.len < num) {
listPush(&dest->list, (struct Data) {0});
}
dest = &dest->list.ptr[num - 1];
}
// Free with bodyParts:
*dest = dataTake(&items.ptr[i + 1]);
}
if (!uid) {
errx(EX_PROTOCOL, "missing UID data item");
}
if (!envelope.subject) {
errx(EX_PROTOCOL, "missing ENVELOPE data item");
}
if (!structure.subtype) {
errx(EX_PROTOCOL, "missing BODYSTRUCTURE data item");
}
if (bodyParts.type == List) {
checkBodyParts(&structure, bodyParts);
}
if (bodyHeader.type == String && bodyText.type == String) {
exportMbox(uid, &envelope, bodyHeader.string, bodyText.string);
}
bool fetch = false;
if (!structure.multipart) {
exportAtom(uid, &envelope, &structure, bodyText);
exportHTML(uid, &envelope, &structure, bodyText);
} else if (bodyParts.type == List) {
exportAtom(uid, &envelope, &structure, bodyParts);
exportHTML(uid, &envelope, &structure, bodyParts);
} else {
fetch = true;
fprintf(
imap, "%s UID FETCH %" PRIu32 " (UID ENVELOPE BODYSTRUCTURE",
Atoms[tag], uid
);
struct List section = {0};
fetchParts(imap, §ion, &structure);
listFree(section);
fprintf(imap, ")\r\n");
}
envelopeFree(envelope);
bodyPartFree(structure);
dataFree(bodyParts);
return fetch;
}