@social-mail/shared 1.1.3 → 1.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/.vscode/settings.json +1 -1
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/mime-parser/HeaderContactList.d.ts +15 -0
  4. package/dist/mime-parser/HeaderContactList.d.ts.map +1 -0
  5. package/dist/mime-parser/HeaderContactList.js +37 -0
  6. package/dist/mime-parser/HeaderContactList.js.map +1 -0
  7. package/dist/mime-parser/HeaderContentDisposition.d.ts +1 -1
  8. package/dist/mime-parser/HeaderContentDisposition.d.ts.map +1 -1
  9. package/dist/mime-parser/HeaderContentDisposition.js +4 -6
  10. package/dist/mime-parser/HeaderContentDisposition.js.map +1 -1
  11. package/dist/mime-parser/HeaderContentType.d.ts +1 -1
  12. package/dist/mime-parser/HeaderContentType.d.ts.map +1 -1
  13. package/dist/mime-parser/HeaderContentType.js +4 -6
  14. package/dist/mime-parser/HeaderContentType.js.map +1 -1
  15. package/dist/mime-parser/HeaderEmailAddress.d.ts +10 -0
  16. package/dist/mime-parser/HeaderEmailAddress.d.ts.map +1 -0
  17. package/dist/mime-parser/HeaderEmailAddress.js +27 -0
  18. package/dist/mime-parser/HeaderEmailAddress.js.map +1 -0
  19. package/dist/mime-parser/HeaderLinks.d.ts +9 -0
  20. package/dist/mime-parser/HeaderLinks.d.ts.map +1 -0
  21. package/dist/mime-parser/HeaderLinks.js +42 -0
  22. package/dist/mime-parser/HeaderLinks.js.map +1 -0
  23. package/dist/mime-parser/MimeEmailAddressParser.d.ts +13 -0
  24. package/dist/mime-parser/MimeEmailAddressParser.d.ts.map +1 -0
  25. package/dist/mime-parser/MimeEmailAddressParser.js +17 -0
  26. package/dist/mime-parser/MimeEmailAddressParser.js.map +1 -0
  27. package/dist/mime-parser/MimeHeaders.d.ts +19 -0
  28. package/dist/mime-parser/MimeHeaders.d.ts.map +1 -0
  29. package/dist/mime-parser/MimeHeaders.js +47 -0
  30. package/dist/mime-parser/MimeHeaders.js.map +1 -0
  31. package/dist/mime-parser/MimeMessage.d.ts +34 -1
  32. package/dist/mime-parser/MimeMessage.d.ts.map +1 -1
  33. package/dist/mime-parser/MimeMessage.js +41 -7
  34. package/dist/mime-parser/MimeMessage.js.map +1 -1
  35. package/dist/mime-parser/MimeNode.d.ts +7 -4
  36. package/dist/mime-parser/MimeNode.d.ts.map +1 -1
  37. package/dist/mime-parser/MimeNode.js +24 -32
  38. package/dist/mime-parser/MimeNode.js.map +1 -1
  39. package/dist/mime-parser/RegexGenerator.d.ts +9 -0
  40. package/dist/mime-parser/RegexGenerator.d.ts.map +1 -0
  41. package/dist/mime-parser/RegexGenerator.js +40 -0
  42. package/dist/mime-parser/RegexGenerator.js.map +1 -0
  43. package/dist/mime-parser/stream/TextWriter.d.ts +1 -0
  44. package/dist/mime-parser/stream/TextWriter.d.ts.map +1 -1
  45. package/dist/mime-parser/stream/TextWriter.js +13 -0
  46. package/dist/mime-parser/stream/TextWriter.js.map +1 -1
  47. package/package.json +1 -1
  48. package/src/mime-parser/HeaderContactList.ts +41 -0
  49. package/src/mime-parser/HeaderContentDisposition.ts +6 -7
  50. package/src/mime-parser/HeaderContentType.ts +6 -7
  51. package/src/mime-parser/HeaderEmailAddress.ts +38 -0
  52. package/src/mime-parser/HeaderLinks.ts +48 -0
  53. package/src/mime-parser/MimeEmailAddressParser.ts +22 -0
  54. package/src/mime-parser/MimeHeaders.ts +62 -0
  55. package/src/mime-parser/MimeMessage.ts +47 -7
  56. package/src/mime-parser/MimeNode.ts +28 -37
  57. package/src/mime-parser/RegexGenerator.ts +42 -0
  58. package/src/mime-parser/stream/TextWriter.ts +14 -0
@@ -0,0 +1,38 @@
1
+ import MimeAddressParser from "./MimeEmailAddressParser.js";
2
+
3
+ export default class HeaderEmailAddress {
4
+
5
+ static parse(text: string) {
6
+ if (/(\<|\>)/i.test(text)) {
7
+ for(const g of MimeAddressParser.parseContactAddressList(text)) {
8
+ const e1 = new HeaderEmailAddress(g.address, g.name);
9
+ return e1;
10
+ }
11
+ }
12
+ const e = new HeaderEmailAddress(text);
13
+ return e;
14
+ }
15
+
16
+ /**
17
+ * Username (part before the @ sign)
18
+ */
19
+ public readonly username: string;
20
+
21
+ /**
22
+ * Domain name part of the email address
23
+ */
24
+ public readonly domain: string;
25
+
26
+ constructor(public emailAddress: string, public name?: string) {
27
+ const r = MimeAddressParser.parseEmailAddress(emailAddress);
28
+ this.domain = r?.domain;
29
+ this.username = r?.user;
30
+ }
31
+
32
+ toString() {
33
+ if (this.name) {
34
+ return `${this.name} <${this.emailAddress}>`;
35
+ }
36
+ return `<${this.emailAddress}>`;
37
+ }
38
+ }
@@ -0,0 +1,48 @@
1
+ export default class HeaderLinks {
2
+
3
+ static parse(value: string) {
4
+ const h = new HeaderLinks();
5
+ const { urls, mailTos, rawLinks } = h;
6
+ for(const line of value.split(/\[\,\;]/g)) {
7
+ if (!line) {
8
+ continue;
9
+ }
10
+ const a = /\<([^\>]+)\>/.exec(line.trim());
11
+ if(!a) {
12
+ continue;
13
+ }
14
+ const s = a[1];
15
+ rawLinks.push(s);
16
+ if(/https?\:\/\//i.test(s)) {
17
+ try {
18
+ urls.push(new URL(s));
19
+ } catch {
20
+ // do nothing...
21
+ }
22
+ continue;
23
+ }
24
+ if(/mailto\:/i.test(s)) {
25
+ try {
26
+ mailTos.push(new URL(s));
27
+ } catch {
28
+ // do nothing..
29
+ }
30
+ }
31
+ }
32
+ return h;
33
+ }
34
+
35
+ public readonly urls = [] as URL[];
36
+
37
+ public readonly mailTos = [] as URL[];
38
+
39
+ public readonly rawLinks = [] as string[];
40
+
41
+ constructor() {
42
+
43
+ }
44
+
45
+ toString() {
46
+ return this.rawLinks.join(",");
47
+ }
48
+ }
@@ -0,0 +1,22 @@
1
+ import RegexGenerator from "./RegexGenerator.js";
2
+
3
+ export const emailAddressHeaderRegex = /(?<name>(("(("")|[^"])+")|([^\<]+)))?\s*\<(?<address>[^\>]+)\>/g;
4
+
5
+ export const emailAddressRegex = /(?<user>(("(""|[^"])+")|([^\@])+))\@(?<domain>[^\s\,\;]*)$/g;
6
+
7
+ export default class MimeAddressParser {
8
+
9
+ public static parseEmailAddress(text: string) {
10
+ const r = emailAddressRegex.exec(text);
11
+ return r?.groups as { user: string, domain: string } ;
12
+ }
13
+
14
+ public static *parseContactAddressList(text: string) {
15
+ const rg = new RegexGenerator(emailAddressHeaderRegex);
16
+ for(const g of rg.groups(text)) {
17
+ const { name, address } = g.groups;
18
+ yield { name, address };
19
+ }
20
+ }
21
+
22
+ }
@@ -0,0 +1,62 @@
1
+ export class MimeHeader {
2
+
3
+ public readonly keyLowerCase: string;
4
+
5
+ private lastParsed: { c, value };
6
+
7
+ constructor(public readonly key: string, public readonly value: string) {
8
+ this.keyLowerCase = key.toLowerCase();
9
+ }
10
+
11
+ as<Parsable>(c: { parse(text: string): Parsable }): Parsable {
12
+ const { lastParsed } = this;
13
+ if (lastParsed?.c === c) {
14
+ return lastParsed.value;
15
+ }
16
+ const value = c.parse(this.value);
17
+ this.lastParsed = { c, value };
18
+ return value as Parsable;
19
+ }
20
+ }
21
+
22
+ /**
23
+ * This class holds the headers in the way they were received.
24
+ * It has different methods to retrive headers.
25
+ * `first` will only display first header.
26
+ * `last` will only display last header in the order.
27
+ */
28
+ export default class MimeHeaders {
29
+
30
+ private headers = [] as MimeHeader[];
31
+
32
+ private map = new Map<string, MimeHeader[]>()
33
+
34
+ public entries() {
35
+ return this.headers[Symbol.iterator]();
36
+ }
37
+
38
+ public append(key: string, value: string) {
39
+ const h = new MimeHeader(key, value);
40
+ this.headers.push(h);
41
+ const lowerKey = key.toLowerCase();
42
+ let existing = this.map.get(lowerKey);
43
+ if (!existing) {
44
+ existing = [];
45
+ this.map.set(lowerKey, existing);
46
+ }
47
+ existing.push(h);
48
+ return h;
49
+ }
50
+
51
+ public set(key: string, value: string) {
52
+ const lowerkey = key.toLowerCase();
53
+ this.headers = this.headers.filter((x) => x.keyLowerCase !== lowerkey);
54
+ this.map.delete(lowerkey);
55
+ this.append(key, value);
56
+ }
57
+
58
+ public first(key: string) {
59
+ return this.map.get(key.toLowerCase())?.[0];
60
+ }
61
+
62
+ }
@@ -2,12 +2,11 @@ import { MimeNode } from "./MimeNode.js";
2
2
  import { AttachmentFile } from "./AttachmentFile.js";
3
3
  import LineStream, { ReadableLineStream, StringLineStream } from "./stream/LineStream.js";
4
4
  import { BlobWriter } from "./stream/TextWriter.js";
5
+ import HeaderLinks from "./HeaderLinks.js";
6
+ import HeaderEmailAddress from "./HeaderEmailAddress.js";
7
+ import HeaderContactList from "./HeaderContactList.js";
5
8
 
6
9
  export default class MimeMessage {
7
- constructor(
8
- private node: MimeNode = new MimeNode("multipart/mixed"),
9
- private attachments: AttachmentFile[] = []
10
- ) {}
11
10
 
12
11
  static async load(
13
12
  reader: ReadableStream | LineStream | string,
@@ -36,7 +35,7 @@ export default class MimeMessage {
36
35
  }
37
36
 
38
37
  get name(): string {
39
- return this.node.header("x-name") || "";
38
+ return this.node.firstHeaderString("x-name") || "";
40
39
  }
41
40
 
42
41
  get html(): string {
@@ -44,6 +43,10 @@ export default class MimeMessage {
44
43
  return alternative?.getFirstChild("text/html")?.text || "";
45
44
  }
46
45
 
46
+ get listUnsubscribe() {
47
+ return this.node.firstHeaderObject("list-unsubscribe")?.as(HeaderLinks);
48
+ }
49
+
47
50
  set html(v: string) {
48
51
  const alternative = this.node.getFirstChild("multipart/alternative", true);
49
52
  const htmlMime = alternative.getFirstChild("text/html", true);
@@ -66,12 +69,49 @@ export default class MimeMessage {
66
69
  }
67
70
 
68
71
  get subject(): string {
69
- return this.node.header("subject") || "";
72
+ return this.node.firstHeaderString("subject") || "";
70
73
  }
71
74
 
72
75
  set subject(v: string) {
73
- this.node.setHeader("subject", v);
76
+ this.node.setHeader("Subject", v);
77
+ }
78
+
79
+ get from(): HeaderEmailAddress {
80
+ return this.node.firstHeaderObject("from")?.as(HeaderEmailAddress);
81
+ }
82
+ set from(v: string | { name: string, emailAddress: string }) {
83
+ if (typeof v === "string") {
84
+ this.node.setHeader("From", v);
85
+ return;
86
+ }
87
+ this.node.setHeader("From", new HeaderEmailAddress(v.emailAddress, v.name).toString());
88
+ }
89
+
90
+ get to(): HeaderContactList {
91
+ return this.node.firstHeaderObject("to")?.as(HeaderContactList);
74
92
  }
93
+ set to(v: string | { name: string, emailAddress: string } | { name: string, emailAddress: string }[]) {
94
+ this.node.setHeader("To", HeaderContactList.from(v).toString());
95
+ }
96
+
97
+ get cc(): HeaderContactList {
98
+ return this.node.firstHeaderObject("cc")?.as(HeaderContactList);
99
+ }
100
+ set cc(v: string | { name: string, emailAddress: string } | { name: string, emailAddress: string }[]) {
101
+ this.node.setHeader("Cc", HeaderContactList.from(v).toString());
102
+ }
103
+
104
+ get bcc(): HeaderContactList {
105
+ return this.node.firstHeaderObject("bcc")?.as(HeaderContactList);
106
+ }
107
+ set bcc(v: string | { name: string, emailAddress: string } | { name: string, emailAddress: string }[]) {
108
+ this.node.setHeader("Bcc", HeaderContactList.from(v).toString());
109
+ }
110
+
111
+ constructor(
112
+ private node: MimeNode = new MimeNode("multipart/mixed"),
113
+ private attachments: AttachmentFile[] = []
114
+ ) {}
75
115
 
76
116
  async save(): Promise<Blob> {
77
117
  const blobWriter = new BlobWriter();
@@ -8,6 +8,7 @@ import { quotedPrintable } from "./encoder/quoted-printable.js";
8
8
  import { wordEncoding } from "./encoder/word-encoding.js";
9
9
  import LineStream from "./stream/LineStream.js";
10
10
  import TextWriter from "./stream/TextWriter.js";
11
+ import MimeHeaders from "./MimeHeaders.js";
11
12
 
12
13
  export class CIMap extends Map<string, any> {
13
14
  private ci = new Map<string, string>();
@@ -93,16 +94,24 @@ export class MimeNode {
93
94
 
94
95
  encoded?: string;
95
96
  children?: MimeNode[];
96
- contentType: HeaderContentType;
97
- contentDisposition?: HeaderContentDisposition;
97
+ get contentType() {
98
+ return this.headers.first("content-type").as(HeaderContentType);
99
+ }
100
+ get contentDisposition() {
101
+ return this.headers.first("content-disposition")?.as(HeaderContentDisposition);
102
+ }
103
+
104
+ set contentDisposition(v: HeaderContentDisposition) {
105
+ this.headers.set("content-disposition", v.toString());
106
+ }
107
+
98
108
  textContent?: string;
99
109
  blobData?: Blob;
100
- headers: CIMap;
110
+ headers: MimeHeaders;
101
111
 
102
112
  constructor(type?: string) {
103
- this.headers = new CIMap();
104
- this.contentType = new HeaderContentType(type);
105
- this.headers.set("Content-Type", this.contentType);
113
+ this.headers = new MimeHeaders();
114
+ this.headers.append("Content-Type", type);
106
115
  }
107
116
 
108
117
  get text(): string {
@@ -153,11 +162,11 @@ export class MimeNode {
153
162
  }
154
163
 
155
164
  get contentTransferEncoding(): string | undefined {
156
- return this.header("Content-Transfer-Encoding");
165
+ return this.firstHeaderString("Content-Transfer-Encoding");
157
166
  }
158
167
 
159
168
  set contentTransferEncoding(v: string | undefined) {
160
- this.setHeader("Content-Transfer-Encoding", v);
169
+ this.headers.set("Content-Transfer-Encoding", v);
161
170
  }
162
171
 
163
172
  get boundary(): string | undefined {
@@ -168,8 +177,12 @@ export class MimeNode {
168
177
  return this.enumerate();
169
178
  }
170
179
 
171
- header(name: string): any {
172
- return this.headers.get(name);
180
+ firstHeaderString(name: string): any {
181
+ return this.headers.first(name)?.value;
182
+ }
183
+
184
+ firstHeaderObject(name: string) {
185
+ return this.headers.first(name);
173
186
  }
174
187
 
175
188
  setHeader(name: string, value: any): void {
@@ -185,7 +198,7 @@ export class MimeNode {
185
198
  this.contentDisposition!.filename!,
186
199
  {
187
200
  type: this.contentType.type,
188
- contentId: this.header("Content-Id"),
201
+ contentId: this.firstHeaderString("Content-Id"),
189
202
  disposition: this.contentDisposition!.type as any
190
203
  });
191
204
  af.node = this;
@@ -283,17 +296,6 @@ export class MimeNode {
283
296
  } else {
284
297
  return;
285
298
  }
286
-
287
- const ct = this.header("Content-Type");
288
- this.contentType.parse(ct);
289
- this.setHeader("Content-Type", ct);
290
-
291
- const cd = this.header("Content-Disposition");
292
- if (cd) {
293
- this.contentDisposition ??= new HeaderContentDisposition();
294
- this.contentDisposition.parse(cd);
295
- this.setHeader("Content-Disposition", cd);
296
- }
297
299
  }
298
300
 
299
301
  async save(writer: TextWriter): Promise<void> {
@@ -305,19 +307,8 @@ export class MimeNode {
305
307
  this.contentType.boundary ||= `${Date.now()}-${Date.now()}-${Date.now()}`;
306
308
  }
307
309
 
308
- for (const [key, element] of this.headers.entries()) {
309
- switch(key) {
310
- case "content-disposition":
311
- writer.writeLine(`Content-Disposition: ${this.contentDisposition}`);
312
- continue;
313
- case "content-type":
314
- writer.writeLine(`Content-Type:${this.contentType.toString()}`);
315
- continue;
316
- case "subject":
317
- writer.writeLine(`${key}: ${wordEncoding.encode(element, true)}`);
318
- continue;
319
- }
320
- writer.writeLine(`${key}: ${element}`);
310
+ for (const h of this.headers.entries()) {
311
+ writer.writeLineMax80Chars(`${h.key}: ${wordEncoding.encode(h.value, true)}`);
321
312
  }
322
313
 
323
314
  writer.writeLine("");
@@ -360,10 +351,10 @@ export class MimeNode {
360
351
  break;
361
352
  case "base64":
362
353
  text = btoa(RawBuffer.encode(text, this.contentType.charset));
363
- writer.writeLine(text);
354
+ writer.writeLineMax80Chars(text);
364
355
  break;
365
356
  default:
366
- writer.writeLine(text);
357
+ writer.writeLineMax80Chars(text);
367
358
  break;
368
359
  }
369
360
  }
@@ -0,0 +1,42 @@
1
+ export default class RegexGenerator{
2
+
3
+ constructor(public readonly regex: RegExp) {
4
+
5
+ }
6
+
7
+ *groups(text: string) {
8
+ if (!text) {
9
+ return;
10
+ }
11
+ const { regex } = this;
12
+ regex.lastIndex = 0;
13
+ let m;
14
+ while((m = regex.exec(text)) !== null) {
15
+ if (m.index === regex.lastIndex) {
16
+ regex.lastIndex++;
17
+ }
18
+ if (m?.length) {
19
+ yield m as RegExpExecArray;
20
+ }
21
+ }
22
+ }
23
+
24
+ *all(text: string) {
25
+ if (!text) {
26
+ return;
27
+ }
28
+ const { regex } = this;
29
+ regex.lastIndex = 0;
30
+ let m;
31
+ while((m = regex.exec(text)) !== null) {
32
+ if (m.index === regex.lastIndex) {
33
+ regex.lastIndex++;
34
+ }
35
+ if (m?.length) {
36
+ yield { matched: m as RegExpExecArray };
37
+ continue;
38
+ }
39
+ }
40
+ }
41
+
42
+ };
@@ -2,6 +2,20 @@ export default class TextWriter {
2
2
  writeLine(line: string): void {
3
3
  throw new Error("Not Implemented");
4
4
  }
5
+
6
+ writeLineMax80Chars(line: string, padPrefix = "") {
7
+ let prefix = "";
8
+ for(;;) {
9
+ if (line.length <= 80) {
10
+ this.writeLine(prefix + line);
11
+ break;
12
+ }
13
+ const firstSegment = line.substring(0, 80);
14
+ this.writeLine(prefix + firstSegment);
15
+ line = line.substring(80);
16
+ prefix = padPrefix;
17
+ }
18
+ }
5
19
  }
6
20
 
7
21
  export class BlobWriter extends TextWriter {