@@ 1,3 1,27 @@
+Parts of hare-message are based on Simon Ser's go-message
+
+MIT License
+
+Copyright (c) 2016 emersion
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
Mozilla Public License Version 2.0
==================================
@@ 1,5 1,13 @@
use ascii;
use bufio;
use bytes;
use errors;
use fmt;
use hash::fnv;
use io;
use strings;
use strio;
use types;
export def HEADER_BUCKETS: u64 = 16;
@@ 25,6 33,37 @@ fn header_field_destroy(hf: *header_field) void = {
free(hf);
};
// Returns the raw representation of this header field, including CRLF. The
// return value is borrowed from the header field.
fn header_field_raw(hf: *header_field) ([]u8 | errors::invalid) = {
if (len(hf.raw) != 0) {
return hf.raw;
};
const iter = strings::iter(hf.key);
for (true) {
const rn = match (strings::next(&iter)) {
case let rn: rune =>
yield rn;
case void =>
break;
};
if (!ascii::isprint(rn) || rn == ':') {
return errors::invalid;
};
};
if (strings::contains(hf.val, "\r\n")) {
return errors::invalid;
};
let sink = bufio::dynamic(io::mode::WRITE);
header_field_fmt(&sink, hf)!;
hf.raw = bufio::buffer(&sink);
return hf.raw;
};
export type header = struct {
fields: []*header_field,
map: [HEADER_BUCKETS][]header_map_key,
@@ 57,11 96,7 @@ export fn header_finish(head: *header) void = {
};
};
// Adds a key, value pair to a [[header]].
export fn header_add(head: *header, key: str, val: str) void = {
const key = canonical_mime_header_key(key);
defer free(key);
fn header_get_mapkey(head: *header, key: str) *header_map_key = {
const i = fnv::string64(key) % HEADER_BUCKETS;
const bucket = &head.map[i];
@@ 73,16 108,23 @@ export fn header_add(head: *header, key: str, val: str) void = {
};
};
let map = match (map) {
match (map) {
case let map: *header_map_key =>
yield map;
return map;
case null =>
append(bucket, header_map_key {
key = strings::dup(key),
...
});
yield &bucket[len(bucket) - 1];
return &bucket[len(bucket) - 1];
};
};
// Adds a key, value pair to a [[header]].
export fn header_add(head: *header, key: str, val: str) void = {
const key = canonical_mime_header_key(key);
defer free(key);
let map = header_get_mapkey(head, key);
const field = alloc(new_header_field(key, val, []));
append(head.fields, field);
@@ 126,6 168,70 @@ export fn header_get(head: *header, key: str) str = {
assert(header_get(&head, "foobar") == "");
};
// Adds a raw key, value pair to a header. The slice should include the complete
// header field including key, value, colon, and CRLF. If the input is not a
// valid MIME header, this function aborts.
export fn header_add_raw(head: *header, kv: []u8) void = {
const colon = bytes::index(kv, ':') as size;
const key = bytes::trim(kv[..colon], ' ', '\t');
const key = canonical_mime_header_key(strings::fromutf8(key)!);
let map = header_get_mapkey(head, key);
const val = decode_header_value(kv[colon..]);
const field = alloc(new_header_field(key, val, alloc(kv...)));
append(head.fields, field);
append(map.fields, field);
};
// Gets the first raw header field associated with a header key, or returns []
// if unset. The return value is borrowed from the header.
export fn header_get_raw(head: *header, key: str) ([]u8 | errors::invalid) = {
const key = canonical_mime_header_key(key);
defer free(key);
const i = fnv::string64(key) % HEADER_BUCKETS;
const bucket = &head.map[i];
for (let i = 0z; i < len(bucket); i += 1) {
const map = &bucket[i];
if (map.key != key) {
continue;
};
return header_field_raw(map.fields[len(map.fields) - 1])?;
};
return [];
};
@test fn header_add_raw() void = {
const head = new_header();
defer header_finish(&head);
const dkim = "DKIM-Signature: a=rsa-sha256; bh=uI/rVH7mLBSWkJVvQYKz3TbpdI2BLZWTIMKcuo0KHOI=; c=simple/simple; d=example.org; h=Subject:To:From; s=default; t=1577562184; v=1; b=;\r\n";
header_add_raw(&head, strings::toutf8(dkim));
const raw = header_get_raw(&head, "DKIM-SIGNature")!;
const raw = strings::fromutf8(raw)!;
assert(raw == dkim);
};
@test fn header_get_raw() void = {
const head = new_header();
defer header_finish(&head);
header_add(&head, "content-type", "text/plain");
header_add(&head, "DKIM-Signature", "a=rsa-sha256; bh=uI/rVH7mLBSWkJVvQYKz3TbpdI2BLZWTIMKcuo0KHOI=; c=simple/simple; d=example.org; h=Subject:To:From; s=default; t=1577562184; v=1; b=;");
const raw = header_get_raw(&head, "Content-Type")!;
const raw = strings::fromutf8(raw)!;
assert(raw == "Content-Type: text/plain\r\n");
const raw = header_get_raw(&head, "DKIM-Signature")!;
const raw = strings::fromutf8(raw)!;
assert(raw == "Dkim-Signature: a=rsa-sha256;\r\n"
" bh=uI/rVH7mLBSWkJVvQYKz3TbpdI2BLZWTIMKcuo0KHOI=; c=simple/simple;\r\n"
" d=example.org; h=Subject:To:From; s=default; t=1577562184; v=1; b=;\r\n");
};
// Returns all of the header values associated with a given key. The returned
// slice must be freed by the caller, but the strings within it are borrowed
// from the header -- use free(), not [[strings::freeall]].
@@ 233,3 339,131 @@ export fn header_set(head: *header, key: str, val: str) void = {
assert(len(vals) == 1);
assert(vals[0] == "text/x-hare");
};
def PREFERRED_HEADER_LEN = 76z;
def MAX_HEADER_LEN = 998z;
// Formats a header field, ensuring each line is no longer than 76 characters,
// folding at whitespace if possible. If a word exceeds this limit, it will be
// split.
fn header_field_fmt(sink: io::handle, hf: *header_field) (size | io::error) = {
let z = fmt::fprintf(sink, "{}: ", hf.key)?;
if (hf.val == "") {
z += fmt::fprint(sink, "\r\n")?;
return z;
};
let first = true;
let val = strings::toutf8(strings::dup(hf.val));
for (len(val) > 0) {
let keylen = 0z;
if (first) {
keylen = z;
};
let (l, next, ok) = fold_line(val, PREFERRED_HEADER_LEN - keylen);
if (!ok) {
let v = fold_line(val, MAX_HEADER_LEN - keylen);
l = v.0; next = v.1;
};
io::write(sink, l)?;
free(l);
free(val);
val = next;
first = false;
};
return z;
};
fn fold_line(v: []u8, max: size) ([]u8, []u8, bool) = {
let ok = true;
let fold_before = max + 1;
let fold_at = len(v);
let folding = "";
if (fold_before > len(v)) {
// End of string
if (v[len(v)-1] != '\n') {
// Add CRLF if not already present
folding = "\r\n";
};
} else {
const needles: [_]u8 = [' ', '\t', '\n'];
fold_at = types::SIZE_MAX;
for (let i = 0z; i < len(needles); i += 1) {
match (bytes::rindex(v[..fold_before], needles[i])) {
case let z: size =>
if (fold_at == types::SIZE_MAX || z > fold_at) {
fold_at = z;
};
case void =>
yield;
};
};
if (fold_at == 0) {
// Whitespace at the previous folding whitespace
fold_at = fold_before - 1;
} else if (fold_at == types::SIZE_MAX) {
// Insert whitespace
fold_at = fold_before - 2;
};
assert(fold_at < len(v));
switch (v[fold_at]) {
case ' ', '\t' =>
if (v[fold_at - 1] != '\n') {
// Next char is whitespace, no need to add one
folding = "\r\n";
};
case '\n' =>
// Already CRLF, nothing to do
folding = "";
case =>
// Another char is present, so add a continuation line.
// This inserts an extra space in the string and should
// be avoided if possible.
folding = "\r\n ";
ok = false;
};
};
let line = alloc(v[..fold_at]...);
append(line, strings::toutf8(folding)...);
let next = alloc(v[fold_at..]...);
return (line, next, ok);
};
// Decodes a header value, collapsing continuation lines as necessary. The
// caller must free the return value.
fn decode_header_value(v: []u8) str = {
const sink = strio::dynamic();
for (true) {
const i = match (bytes::index(v, '\n')) {
case void =>
write_continued(&sink, v);
break;
case let z: size =>
yield z;
};
write_continued(&sink, v[..i]);
v = v[i+1..];
};
return strio::string(&sink);
};
fn write_continued(sink: *strio::stream, v: []u8) void = {
if (len(v) > 0 && v[len(v)-1] == '\r') {
v = v[..len(v)-1];
};
v = bytes::trim(v, ' ', '\t');
if (len(v) == 0) {
return;
};
if (len(strio::string(sink)) != 0) {
strio::appendrune(sink, ' ')!;
};
io::write(sink, v)!;
};