@lowdefy/connection-smtp 6.1.0 → 7.1.0

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.
@@ -16,6 +16,10 @@
16
16
  import schema from './schema.js';
17
17
  export default {
18
18
  schema,
19
+ // An outside service, not app data: a journey on a data set keeps its real target.
20
+ meta: {
21
+ dataSet: 'external'
22
+ },
19
23
  requests: {
20
24
  SMTPMailSend
21
25
  }
@@ -12,27 +12,99 @@
12
12
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
- */ import { type } from '@lowdefy/helpers';
15
+ */ // eslint-plugin-import's node resolver does not read package exports maps.
16
+ // eslint-disable-next-line import/no-unresolved
17
+ import addressparser from 'nodemailer/lib/addressparser';
18
+ import { type } from '@lowdefy/helpers';
19
+ // nodemailer reads a string recipient as a comma-separated list, so a string holding several
20
+ // addresses is split into one entry per address, with nodemailer's own parser. A string holding
21
+ // one address is kept as written.
22
+ function splitEntries(entries) {
23
+ if (type.isNone(entries)) {
24
+ return [];
25
+ }
26
+ return (type.isArray(entries) ? entries : [
27
+ entries
28
+ ]).flatMap((entry)=>{
29
+ if (!type.isString(entry)) {
30
+ return [
31
+ entry
32
+ ];
33
+ }
34
+ const parsed = addressparser(entry, {
35
+ flatten: true
36
+ });
37
+ return parsed.length > 1 ? parsed : [
38
+ entry
39
+ ];
40
+ });
41
+ }
42
+ // An entry from splitEntries: a string with one address, a request object ({ name, email }) or
43
+ // a parsed nodemailer object ({ name, address }).
16
44
  function getBareEmail(entry) {
17
45
  if (type.isObject(entry)) {
18
- return String(entry.email ?? '').toLowerCase();
46
+ return String(entry.address ?? entry.email ?? '').trim().toLowerCase();
47
+ }
48
+ if (!type.isString(entry)) {
49
+ return '';
50
+ }
51
+ const [parsed] = addressparser(entry, {
52
+ flatten: true
53
+ });
54
+ return (parsed?.address ?? '').trim().toLowerCase();
55
+ }
56
+ function getDomain(email) {
57
+ return email.slice(email.lastIndexOf('@') + 1);
58
+ }
59
+ // A loop, not /\.+$/: that regex takes quadratic time on a long run of dots, and the domain comes
60
+ // from the request.
61
+ function trimTrailingDots(domain) {
62
+ let end = domain.length;
63
+ while(end > 0 && domain[end - 1] === '.'){
64
+ end -= 1;
65
+ }
66
+ return domain.slice(0, end);
67
+ }
68
+ // .invalid is reserved and never resolves (RFC 6761). A trailing dot is the fully qualified
69
+ // form of the same domain.
70
+ function isInvalid(entry) {
71
+ const domain = trimTrailingDots(getDomain(getBareEmail(entry)));
72
+ return domain === 'invalid' || domain.endsWith('.invalid');
73
+ }
74
+ // Returns the field as written when nothing is dropped, so mail without .invalid recipients
75
+ // reaches nodemailer unchanged.
76
+ function dropInvalid(entries) {
77
+ const split = splitEntries(entries);
78
+ const kept = split.filter((entry)=>!isInvalid(entry));
79
+ if (kept.length === split.length) {
80
+ return entries;
19
81
  }
20
- const angleMatch = /<([^<>]+)>/.exec(String(entry));
21
- if (angleMatch) {
22
- return angleMatch[1].toLowerCase();
82
+ return kept.length === 0 ? undefined : kept;
83
+ }
84
+ // Mail to .invalid is dropped on every send, filter or not, and only those recipients are
85
+ // dropped: a real cc or bcc recipient still gets the mail when every to recipient is .invalid.
86
+ // Returns null when no recipient is left.
87
+ function dropInvalidRecipients(mail) {
88
+ const to = dropInvalid(mail.to);
89
+ const cc = dropInvalid(mail.cc);
90
+ const bcc = dropInvalid(mail.bcc);
91
+ if (type.isNone(to) && type.isNone(cc) && type.isNone(bcc)) {
92
+ return null;
23
93
  }
24
- return String(entry).toLowerCase();
94
+ return {
95
+ ...mail,
96
+ to,
97
+ cc,
98
+ bcc
99
+ };
25
100
  }
26
101
  function createMatcher({ allowlist, regex }) {
27
102
  const allowedDomains = type.isNone(allowlist) ? null : allowlist.map((domain)=>domain.toLowerCase());
28
103
  const pattern = type.isNone(regex) ? null : new RegExp(regex);
29
104
  return function matches(entry) {
30
105
  const email = getBareEmail(entry);
31
- if (allowedDomains !== null) {
32
- const domain = email.slice(email.lastIndexOf('@') + 1);
33
- if (!allowedDomains.includes(domain)) {
34
- return false;
35
- }
106
+ if (allowedDomains !== null && !allowedDomains.includes(getDomain(email))) {
107
+ return false;
36
108
  }
37
109
  if (pattern !== null && !pattern.test(email)) {
38
110
  return false;
@@ -41,14 +113,13 @@ function createMatcher({ allowlist, regex }) {
41
113
  };
42
114
  }
43
115
  function filterEntries({ entries, matches }) {
44
- if (type.isNone(entries)) {
45
- return [];
46
- }
47
- return (type.isArray(entries) ? entries : [
48
- entries
49
- ]).filter(matches);
116
+ return splitEntries(entries).filter(matches);
50
117
  }
51
- function applyMailFilter({ filter, mail }) {
118
+ function applyMailFilter({ filter, mail: received }) {
119
+ const mail = dropInvalidRecipients(received);
120
+ if (mail === null) {
121
+ return null;
122
+ }
52
123
  if (type.isNone(filter)) {
53
124
  return mail;
54
125
  }
@@ -103,7 +103,8 @@ export default {
103
103
  // null allowed on all filter values because they come from _secret;
104
104
  // unset secrets resolve null - an unset filter field is disabled. An unset
105
105
  // (null) filter falls back to the current environment's email filter;
106
- // false turns filtering off, the environment's too.
106
+ // false turns filtering off, the environment's too. Recipients on the reserved
107
+ // .invalid domain are always dropped (applyMailFilter.js).
107
108
  filter: {
108
109
  type: [
109
110
  'object',
package/dist/types.js CHANGED
@@ -16,6 +16,13 @@
16
16
  connections: [
17
17
  'SMTP'
18
18
  ],
19
+ // Non-scopable: the tenant wall does not cover this type, so its
20
+ // connections are never scoped and accept no tenant: declaration.
21
+ connectionMetas: {
22
+ SMTP: {
23
+ tenant: false
24
+ }
25
+ },
19
26
  requests: [
20
27
  'SMTPMailSend'
21
28
  ]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lowdefy/connection-smtp",
3
- "version": "6.1.0",
3
+ "version": "7.1.0",
4
4
  "license": "Apache-2.0",
5
5
  "description": "",
6
6
  "homepage": "https://lowdefy.com",
@@ -27,8 +27,12 @@
27
27
  "url": "https://github.com/lowdefy/lowdefy.git"
28
28
  },
29
29
  "type": "module",
30
+ "sideEffects": [
31
+ "**/*.css"
32
+ ],
30
33
  "exports": {
31
34
  "./connections": "./dist/connections.js",
35
+ "./send": "./dist/connections/SMTP/send.js",
32
36
  "./types": "./dist/types.js",
33
37
  "./transportSchema": "./dist/connections/SMTP/transportSchema.js"
34
38
  },
@@ -36,12 +40,12 @@
36
40
  "dist/*"
37
41
  ],
38
42
  "dependencies": {
39
- "@lowdefy/helpers": "6.1.0",
40
- "nodemailer": "9.1.1"
43
+ "@lowdefy/helpers": "7.1.0",
44
+ "nodemailer": "10.0.13"
41
45
  },
42
46
  "devDependencies": {
43
47
  "@jest/globals": "28.1.3",
44
- "@lowdefy/ajv": "6.1.0",
48
+ "@lowdefy/ajv": "7.1.0",
45
49
  "@swc/cli": "0.8.1",
46
50
  "@swc/core": "1.15.32",
47
51
  "@swc/jest": "0.2.39",