Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat/encoders & feat/related-attachments #63

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,7 @@ export interface SendConfig {
content?: string;
mimeContent?: Content[];
html?: string;
relatedAttachments?: Attachment[];
inReplyTo?: string;
replyTo?: string;
references?: string;
Expand Down Expand Up @@ -302,6 +303,10 @@ base64 is converted to binary and is only present for a better API. So don't
encode your binary files as base64, otherwise denomailer can't convert it back
to binary.

#### relatedAttachments

Attachments related to the html content, for example embedded images.

#### internalTag

This can be used with preprocessors so you can give mail a type, for example
Expand Down
210 changes: 104 additions & 106 deletions client/basic/client.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import type { ResolvedSendConfig } from "../../config/mail/mod.ts";
import type { ResolvedContent } from "../../config/mail/content.ts";
import type { ResolvedAttachment } from "../../config/mail/attachments.ts";
import { ResolvedClientOptions } from "../../config/client.ts";
import { SMTPConnection } from "./connection.ts";
import { QUE } from "./QUE.ts";
Expand Down Expand Up @@ -53,6 +55,98 @@ export class SMTPClient {
return this.#que.idle;
}

calcBoundary(content: string, pattern: RegExp): number {
let boundary = 100;

const matches = content.matchAll(pattern);
for (const match of matches) {
boundary += parseInt(match[1], 10);
}

return boundary;
}

encodeContent(content: ResolvedContent) {
this.#connection.writeCmd(
"Content-Type: " + content.mimeType,
);
if (content.transferEncoding) {
this.#connection.writeCmd(
`Content-Transfer-Encoding: ${content.transferEncoding}\r\n`,
);
} else {
this.#connection.writeCmd("");
}

this.#connection.writeCmd(content.content + "\r\n");
}

encodeAttachment(attachment: ResolvedAttachment) {
this.#connection.writeCmd(
`Content-Type: ${attachment.contentType}; name=${attachment.filename}`,
);

if (attachment.contentID) {
this.#connection.writeCmd(
`Content-ID: <${attachment.contentID}>`,
);
}

this.#connection.writeCmd(
`Content-Disposition: ${attachment.contentDisposition}; filename=${attachment.filename}`,
);

if (attachment.encoding === "base64") {
this.#connection.writeCmd(
"Content-Transfer-Encoding: base64\r\n",
);

for (
let line = 0;
line < Math.ceil(attachment.content.length / 75);
line++
) {
const lineOfBase64 = attachment.content.slice(
line * 75,
(line + 1) * 75,
);

this.#connection.writeCmd(lineOfBase64);
}

this.#connection.writeCmd("\r\n");
} else if (attachment.encoding === "text") {
this.#connection.writeCmd(
"Content-Transfer-Encoding: quoted-printable\r\n",
);

this.#connection.writeCmd(attachment.content + "\r\n");
}
}

encodeRelated(content: ResolvedContent) {
const boundaryAddRel = this.calcBoundary(
content.content + "\n" +
content.relatedAttachments.map((v) => v.content).join("\n"),
new RegExp("--related([0-9]+)", "g"),
);

const relatedBoundary = `related${boundaryAddRel}`;
this.#connection.writeCmd(
`Content-Type: multipart/related; boundary=${relatedBoundary}\r\n; type=${content.mimeType}`,
);

this.#connection.writeCmd(`--${relatedBoundary}`);
this.encodeContent(content);

for (let i = 0; i < content.relatedAttachments.length; i++) {
this.#connection.writeCmd(`--${relatedBoundary}`);
this.encodeAttachment(content.relatedAttachments[i]);
}

this.#connection.writeCmd(`--${relatedBoundary}--\r\n`);
}

async send(config: ResolvedSendConfig) {
await this.#ready;
let dataMode = false;
Expand Down Expand Up @@ -144,34 +238,10 @@ export class SMTPClient {

this.#connection.writeCmd("MIME-Version: 1.0");

let boundaryAdditionAtt = 100;
// calc msg boundary
// TODO: replace this with a match or so.
config.mimeContent.map((v) => v.content).join("\n").replace(
new RegExp("--attachment([0-9]+)", "g"),
(_, numb) => {
boundaryAdditionAtt += parseInt(numb, 10);

return "";
},
);

// const dec = new TextDecoder();

config.attachments.map((v) => {
return v.content;
// if (v.encoding === "text") return v.content;

// const arr = new Uint8Array(v.content);

// return dec.decode(arr);
}).join("\n").replace(
const boundaryAdditionAtt = this.calcBoundary(
config.mimeContent.map((v) => v.content).join("\n") + "\n" +
config.attachments.map((v) => v.content).join("\n"),
new RegExp("--attachment([0-9]+)", "g"),
(_, numb) => {
boundaryAdditionAtt += parseInt(numb, 10);

return "";
},
);

const attachmentBoundary = `attachment${boundaryAdditionAtt}`;
Expand All @@ -182,19 +252,12 @@ export class SMTPClient {
);
this.#connection.writeCmd(`--${attachmentBoundary}`);

let boundaryAddition = 100;
// calc msg boundary
// TODO: replace this with a match or so.
config.mimeContent.map((v) => v.content).join("\n").replace(
const boundaryAdditionMsg = this.calcBoundary(
config.mimeContent.map((v) => v.content).join("\n"),
new RegExp("--message([0-9]+)", "g"),
(_, numb) => {
boundaryAddition += parseInt(numb, 10);

return "";
},
);

const messageBoundary = `message${boundaryAddition}`;
const messageBoundary = `message${boundaryAdditionMsg}`;

this.#connection.writeCmd(
`Content-Type: multipart/alternative; boundary=${messageBoundary}`,
Expand All @@ -203,83 +266,18 @@ export class SMTPClient {

for (let i = 0; i < config.mimeContent.length; i++) {
this.#connection.writeCmd(`--${messageBoundary}`);
this.#connection.writeCmd(
"Content-Type: " + config.mimeContent[i].mimeType,
);
if (config.mimeContent[i].transferEncoding) {
this.#connection.writeCmd(
`Content-Transfer-Encoding: ${
config.mimeContent[i].transferEncoding
}` + "\r\n",
);
if (config.mimeContent[i].relatedAttachments.length === 0) {
this.encodeContent(config.mimeContent[i]);
} else {
// Send new line
this.#connection.writeCmd("");
this.encodeRelated(config.mimeContent[i]);
}

this.#connection.writeCmd(config.mimeContent[i].content, "\r\n");
}

this.#connection.writeCmd(`--${messageBoundary}--\r\n`);

for (let i = 0; i < config.attachments.length; i++) {
const attachment = config.attachments[i];

this.#connection.writeCmd(`--${attachmentBoundary}`);
this.#connection.writeCmd(
"Content-Type:",
attachment.contentType + ";",
"name=" + attachment.filename,
);

if (attachment.contentID) {
this.#connection.writeCmd(
`Content-ID: <${attachment.contentID}>`,
);
}

this.#connection.writeCmd(
"Content-Disposition: attachment; filename=" + attachment.filename,
);

if (attachment.encoding === "base64") {
this.#connection.writeCmd(
"Content-Transfer-Encoding: base64\r\n",
);

for (
let line = 0;
line < Math.ceil(attachment.content.length / 75);
line++
) {
const lineOfBase64 = attachment.content.slice(
line * 75,
(line + 1) * 75,
);

this.#connection.writeCmd(lineOfBase64);
}

// if (
// attachment.content instanceof ArrayBuffer ||
// attachment.content instanceof SharedArrayBuffer
// ) {
// await this.#connection.writeCmdBinary(
// new Uint8Array(attachment.content),
// );
// } else {
// await this.#connection.writeCmdBinary(attachment.content);
// }

this.#connection.writeCmd("\r\n");
} else if (attachment.encoding === "text") {
this.#connection.writeCmd(
"Content-Transfer-Encoding: quoted-printable",
"\r\n",
);

this.#connection.writeCmd(attachment.content, "\r\n");
}
this.encodeAttachment(config.attachments[i]);
}

this.#connection.writeCmd(`--${attachmentBoundary}--\r\n`);
Expand Down
15 changes: 12 additions & 3 deletions config/mail/attachments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,16 @@ export type Attachment =
| base64Attachment
| arrayBufferLikeAttachment
)
& baseAttachment;
& baseAttachment
& { contentDisposition?: "attachment" | "inline" };

export type ResolvedAttachment =
& (
| textAttachment
| base64Attachment
)
& baseAttachment;
& baseAttachment
& { contentDisposition: "attachment" | "inline" };

type textAttachment = { encoding: "text"; content: string };
type base64Attachment = { encoding: "base64"; content: string };
Expand All @@ -35,8 +37,15 @@ export function resolveAttachment(attachment: Attachment): ResolvedAttachment {
contentType: attachment.contentType,
encoding: "base64",
content: base64Encode(attachment.content),
contentDisposition: attachment.contentDisposition ?? "attachment",
};
} else {
return attachment;
return {
filename: attachment.filename,
contentType: attachment.contentType,
encoding: attachment.encoding,
content: attachment.content,
contentDisposition: attachment.contentDisposition ?? "attachment",
};
}
}
38 changes: 33 additions & 5 deletions config/mail/content.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,47 @@
import {
Attachment,
resolveAttachment,
ResolvedAttachment,
} from "./attachments.ts";
import { quotedPrintableEncode } from "./encoding.ts";

export interface Content {
mimeType: string;
content: string;
transferEncoding?: string;
relatedAttachments?: Attachment[];
}

export function resolveContent({
export interface ResolvedContent {
mimeType: string;
content: string;
transferEncoding?: string;
relatedAttachments: ResolvedAttachment[];
}

export function resolveContent(content: Content): ResolvedContent {
return {
mimeType: content.mimeType,
content: content.content,
transferEncoding: content.transferEncoding,
relatedAttachments: content.relatedAttachments
? content.relatedAttachments.map((v) => resolveAttachment(v))
: [],
};
}

export function resolveMessage({
text,
html,
relatedAttachments,
mimeContent,
}: {
text?: string;
html?: string;
relatedAttachments?: Attachment[];
mimeContent?: Content[];
}): Content[] {
const newContent = [...mimeContent ?? []];
}): ResolvedContent[] {
const newContent = [...mimeContent ?? []].map((v) => resolveContent(v));

if (text === "auto" && html) {
text = html
Expand All @@ -29,15 +55,17 @@ export function resolveContent({
mimeType: 'text/plain; charset="utf-8"',
content: quotedPrintableEncode(text),
transferEncoding: "quoted-printable",
relatedAttachments: [],
});
}

if (html) {
newContent.push({
newContent.push(resolveContent({
mimeType: 'text/html; charset="utf-8"',
content: quotedPrintableEncode(html),
transferEncoding: "quoted-printable",
});
relatedAttachments,
}));
}

return newContent;
Expand Down
Loading