#! /bin/sh # usage: parts list # usage: parts add PATH [NAME [DESCRIPTION]] set -efu list_parts() { jq -r ' def replicate(n; x): if n == 0 then "" else n * x end ; def flattenMap(inner): def addDepth(depth): if type == "array" then map(addDepth(depth + 1)) else { depth: depth, value: . } end ; def addIndex: to_entries | map(.value + { index: .key }) ; def getPrefix: match("( *).*").captures[0].string ; def stripPrefix: match(" *(.*)").captures[0].string ; def shiftPrefix: match("( *[^ ]+)( *)( .*)").captures|map(.string)|.[1]+.[0]+.[2] ; addDepth(0) | flatten | addIndex | map(inner) | map(shiftPrefix) ; def parseContentType: if . != null then . else "text/plain; charset=UTF-8" end | split(";\\s*";"") | (.[0] | split("/")) as $compoundType | $compoundType[0] as $type | $compoundType[1] as $subtype | (.[1:] | map(split("=") | { key: .[0], value: .[1] }) | from_entries) as $params | { $type, $subtype, $params, } ; def formatPart: .body as $body | ((.headers // []) | from_entries) as $headers | ($headers["content-type"] | parseContentType) as $ct | "\($ct.type)/\($ct.subtype)" as $compoundType | $ct.params.name as $name | [ "\($compoundType) \($name | tojson) \($body | length)" ] + if $ct.type == "multipart" then .body | map(formatPart) else [] end ; formatPart | flattenMap("\(replicate(.depth - 1; " "))part#\(.index + 1) \(.value)")[] ' } add_part() {( filepath=$1 filename=${2-$(basename "$filepath")} description=${3-$filename} contentType="$(file -Lib "$filepath"); name=$filename" case $contentType in text/plain|text/plain\;*) max_length=$(awk ' length > max_length { max_length = length } END { print max_length } ') if test "$max_length" -le 998; then contentTransferEncoding=8bit content() { cat "$filepath" } else contentTransferEncoding=base64 content() { base64 "$filepath" } fi ;; *) contentTransferEncoding=base64 content() { base64 "$filepath" } ;; esac contentDisposition="attachment; filename=$filename" contentDescription=$description boundary=$(date -Ins | sha1sum | cut -d\ -f1) { jq '{ mail: . }' content | jq -Rs '{ content: . }' } | jq -sr \ --arg boundary "$boundary" \ --arg contentType "$contentType" \ --arg contentTransferEncoding "$contentTransferEncoding" \ --arg contentDisposition "$contentDisposition" \ --arg contentDescription "$contentDescription" \ ' add | .mail as $mail | .content as $content | def parseContentType: if . == null then null else split(";\\s*";"") | (.[0] | split("/")) as $type | (.[1:] | map(split("=") | { key: .[0], value: .[1] }) | from_entries) as $parameters | { type: $type[0], subtype: $type[1], parameters: $parameters, } end; ($mail.headers | from_entries) as $headers | ($headers["content-type"] | if . == null then { type: "text", subtype: "plain", parameters: { charset: "UTF-8", }, } else parseContentType end) as $ct | def add_part: { headers: ({ "content-type": $contentType, "content-transfer-encoding": $contentTransferEncoding, "content-disposition": $contentDisposition, "content-description": $contentDescription, } | to_entries), body: $content, } as $part | { headers: .headers , body: (.body + [$part]) }; ($mail.headers | from_entries) as $headers | def is_multipart_mixed: $headers["content-type"] // "" | test("^multipart/mixed\\b.*"); if is_multipart_mixed then $mail | add_part else { headers: ( $mail.headers | map(select(.key != "mime-version")) | map(select(.key != "content-type")) | map(select(.key != "content-transfer-encoding")) | . + ({ "mime-version": "1.0", "Content-Type": "multipart/mixed; boundary=\"----=\($boundary)\"", } | to_entries) ) , body: [{ headers: ({ "content-type": ($headers["content-type"] // "text/plain; charset=UTF-8"), "content-transfer-encoding": ($headers["content-transfer-encoding"] // "8bit"), } | to_entries), body: $mail.body, }] } | add_part end ' )} command=$1; shift case $command in list) mailaid | list_parts ;; add) mailaid | add_part "$@" | mailaid -d ;; *) echo "$0: error: unknown command: $command" >&2 exit 1 ;; esac