@mailmodo/cli 0.0.20-beta.pr22.37 → 0.0.20-beta.pr23.33
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.
- package/dist/commands/deploy/index.d.ts +0 -3
- package/dist/commands/deploy/index.js +20 -29
- package/dist/commands/domain/index.js +6 -11
- package/dist/commands/edit/index.d.ts +0 -10
- package/dist/commands/edit/index.js +9 -127
- package/dist/commands/preview/index.js +2 -7
- package/dist/commands/settings/index.js +21 -4
- package/dist/lib/constants.d.ts +1 -1
- package/dist/lib/constants.js +1 -1
- package/oclif.manifest.json +1 -1
- package/package.json +1 -1
|
@@ -16,9 +16,6 @@ export default class Deploy extends BaseCommand {
|
|
|
16
16
|
run(): Promise<void>;
|
|
17
17
|
private buildDeployPayload;
|
|
18
18
|
private mapEmailToPayload;
|
|
19
|
-
private buildBrandSection;
|
|
20
|
-
private buildProductSection;
|
|
21
|
-
private buildSenderSection;
|
|
22
19
|
private buildProjectPayload;
|
|
23
20
|
private confirmDeploy;
|
|
24
21
|
private ensureDomainReady;
|
|
@@ -77,39 +77,30 @@ export default class Deploy extends BaseCommand {
|
|
|
77
77
|
trigger: email.trigger,
|
|
78
78
|
};
|
|
79
79
|
}
|
|
80
|
-
buildBrandSection(project) {
|
|
81
|
-
return {
|
|
82
|
-
colors: [project?.brandColor || DEFAULT_BRAND_COLOR],
|
|
83
|
-
logoUrl: project?.logoUrl || '',
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
buildProductSection(project) {
|
|
87
|
-
return {
|
|
88
|
-
businessType: project?.type || '',
|
|
89
|
-
description: '',
|
|
90
|
-
pricingModel: '',
|
|
91
|
-
productName: project?.name || '',
|
|
92
|
-
saasModel: '',
|
|
93
|
-
targetUser: '',
|
|
94
|
-
url: project?.url || '',
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
buildSenderSection(project) {
|
|
98
|
-
return {
|
|
99
|
-
address: project?.address || '',
|
|
100
|
-
domain: project?.domain || '',
|
|
101
|
-
fromEmail: project?.fromEmail || '',
|
|
102
|
-
fromName: project?.fromName || '',
|
|
103
|
-
replyTo: project?.replyTo || project?.fromEmail || '',
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
80
|
buildProjectPayload(project) {
|
|
107
81
|
return {
|
|
108
|
-
brand:
|
|
82
|
+
brand: {
|
|
83
|
+
colors: [project?.brandColor || DEFAULT_BRAND_COLOR],
|
|
84
|
+
logoUrl: project?.logoUrl || '',
|
|
85
|
+
},
|
|
109
86
|
emailStyle: project?.emailStyle || 'branded',
|
|
110
87
|
monthlyCap: project?.monthlyCap ?? DEFAULT_MONTHLY_CAP,
|
|
111
|
-
product:
|
|
112
|
-
|
|
88
|
+
product: {
|
|
89
|
+
businessType: project?.type || '',
|
|
90
|
+
description: '',
|
|
91
|
+
pricingModel: '',
|
|
92
|
+
productName: project?.name || '',
|
|
93
|
+
saasModel: '',
|
|
94
|
+
targetUser: '',
|
|
95
|
+
url: project?.url || '',
|
|
96
|
+
},
|
|
97
|
+
senderDetails: {
|
|
98
|
+
address: project?.address || '',
|
|
99
|
+
domain: project?.domain || '',
|
|
100
|
+
fromEmail: project?.fromEmail || '',
|
|
101
|
+
fromName: project?.fromName || '',
|
|
102
|
+
replyTo: project?.replyTo || project?.fromEmail || '',
|
|
103
|
+
},
|
|
113
104
|
webhookUrl: project?.webhookUrl || '',
|
|
114
105
|
};
|
|
115
106
|
}
|
|
@@ -124,15 +124,15 @@ export default class Domain extends BaseCommand {
|
|
|
124
124
|
if (!response.ok) {
|
|
125
125
|
this.handleApiError(response);
|
|
126
126
|
}
|
|
127
|
-
const { dkim, dmarc,
|
|
127
|
+
const { dkim, dmarc, spf } = response.data;
|
|
128
128
|
if (jsonOutput) {
|
|
129
|
-
this.log(JSON.stringify({ dkim, dmarc,
|
|
129
|
+
this.log(JSON.stringify({ dkim, dmarc, spf }, null, 2));
|
|
130
130
|
return;
|
|
131
131
|
}
|
|
132
|
-
this.log(`
|
|
133
|
-
this.log(`
|
|
134
|
-
this.log(`
|
|
135
|
-
const allPassed =
|
|
132
|
+
this.log(` SPF ${spf ? chalk.green('✓') : chalk.red('✗ Not found')}`);
|
|
133
|
+
this.log(` DKIM ${dkim ? chalk.green('✓') : chalk.red('✗ Not found')}`);
|
|
134
|
+
this.log(` DMARC ${dmarc ? chalk.green('✓') : chalk.red('✗ Not found')}`);
|
|
135
|
+
const allPassed = spf && dkim && dmarc;
|
|
136
136
|
if (allPassed) {
|
|
137
137
|
this.log(`\n ${chalk.green('✓')} Domain verified.\n`);
|
|
138
138
|
}
|
|
@@ -144,11 +144,6 @@ export default class Domain extends BaseCommand {
|
|
|
144
144
|
this.log(` - Including the full domain in the Host field`);
|
|
145
145
|
this.log(` - Cloudflare: proxy must be OFF (grey cloud, not orange)`);
|
|
146
146
|
}
|
|
147
|
-
if (!returnPath) {
|
|
148
|
-
this.log(`\n Return Path common mistakes:`);
|
|
149
|
-
this.log(` - Missing or incorrect CNAME for mm-bounce subdomain`);
|
|
150
|
-
this.log(` - Cloudflare: proxy must be OFF (grey cloud, not orange)`);
|
|
151
|
-
}
|
|
152
147
|
this.log(`\n Fix the records and run ${chalk.cyan('mailmodo domain --verify')} again.`);
|
|
153
148
|
this.log(` Help: ${chalk.cyan(DNS_GUIDE_URL)}\n`);
|
|
154
149
|
}
|
|
@@ -10,15 +10,5 @@ export default class Edit extends BaseCommand {
|
|
|
10
10
|
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
11
|
yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
12
12
|
};
|
|
13
|
-
private showFieldDiff;
|
|
14
|
-
private stripHtml;
|
|
15
|
-
private truncate;
|
|
16
|
-
private showHtmlChange;
|
|
17
|
-
private showUnchangedField;
|
|
18
|
-
private showUnchangedHtml;
|
|
19
|
-
private showSuggestedChanges;
|
|
20
|
-
private showUnchanged;
|
|
21
|
-
private buildDiffPreview;
|
|
22
|
-
private showChangeSummary;
|
|
23
13
|
run(): Promise<void>;
|
|
24
14
|
}
|
|
@@ -22,123 +22,6 @@ export default class Edit extends BaseCommand {
|
|
|
22
22
|
description: 'Natural language description of the change',
|
|
23
23
|
}),
|
|
24
24
|
};
|
|
25
|
-
showFieldDiff(label, oldVal, newVal) {
|
|
26
|
-
if (!newVal || oldVal === newVal)
|
|
27
|
-
return false;
|
|
28
|
-
this.log(`\n ${label}:`);
|
|
29
|
-
if (oldVal)
|
|
30
|
-
this.log(` ${chalk.red(`- ${oldVal}`)}`);
|
|
31
|
-
this.log(` ${chalk.green(`+ ${newVal}`)}`);
|
|
32
|
-
return true;
|
|
33
|
-
}
|
|
34
|
-
stripHtml(html) {
|
|
35
|
-
return html
|
|
36
|
-
.replaceAll(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
|
|
37
|
-
.replaceAll(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
|
|
38
|
-
.replaceAll(/<[^>]+>/g, ' ')
|
|
39
|
-
.replaceAll(' ', ' ')
|
|
40
|
-
.replaceAll('&', '&')
|
|
41
|
-
.replaceAll('<', '<')
|
|
42
|
-
.replaceAll('>', '>')
|
|
43
|
-
.replaceAll(/\s+/g, ' ')
|
|
44
|
-
.trim();
|
|
45
|
-
}
|
|
46
|
-
truncate(text, max) {
|
|
47
|
-
return text.length > max ? `${text.slice(0, max)}…` : text;
|
|
48
|
-
}
|
|
49
|
-
showHtmlChange(oldHtml, newHtml) {
|
|
50
|
-
if (!newHtml || oldHtml === newHtml)
|
|
51
|
-
return false;
|
|
52
|
-
this.log(`\n HTML Body:`);
|
|
53
|
-
const MAX = 500;
|
|
54
|
-
if (oldHtml) {
|
|
55
|
-
const oldText = this.truncate(this.stripHtml(oldHtml), MAX);
|
|
56
|
-
this.log(` ${chalk.red(`- ${oldText}`)}`);
|
|
57
|
-
}
|
|
58
|
-
const newText = this.truncate(this.stripHtml(newHtml), MAX);
|
|
59
|
-
this.log(` ${chalk.green(`+ ${newText}`)}`);
|
|
60
|
-
return true;
|
|
61
|
-
}
|
|
62
|
-
showUnchangedField(label, value) {
|
|
63
|
-
if (!value)
|
|
64
|
-
return;
|
|
65
|
-
this.log(`\n ${label}:`);
|
|
66
|
-
this.log(` ${chalk.dim(value)}`);
|
|
67
|
-
}
|
|
68
|
-
showUnchangedHtml(templateHtml) {
|
|
69
|
-
if (!templateHtml)
|
|
70
|
-
return;
|
|
71
|
-
this.log(`\n HTML Body:`);
|
|
72
|
-
this.log(` ${chalk.dim(this.truncate(this.stripHtml(templateHtml), 500))}`);
|
|
73
|
-
}
|
|
74
|
-
showSuggestedChanges(email, updated, templateHtml, changed) {
|
|
75
|
-
this.log('\n Suggested Changes:');
|
|
76
|
-
if (changed.subject)
|
|
77
|
-
this.showFieldDiff('Subject', email.subject, updated.subject);
|
|
78
|
-
if (changed.preview)
|
|
79
|
-
this.showFieldDiff('Preview Text', email.previewText, updated.previewText);
|
|
80
|
-
if (changed.html)
|
|
81
|
-
this.showHtmlChange(templateHtml, updated.html);
|
|
82
|
-
if (changed.cta)
|
|
83
|
-
this.showFieldDiff('CTA Text', undefined, updated.ctaText);
|
|
84
|
-
if (!changed.subject && !changed.preview && !changed.html && !changed.cta) {
|
|
85
|
-
this.log(`\n ${chalk.dim('No changes detected.')}`);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
showUnchanged(email, templateHtml, changed) {
|
|
89
|
-
const hasContent = !changed.subject ||
|
|
90
|
-
(!changed.preview && Boolean(email.previewText)) ||
|
|
91
|
-
(!changed.html && Boolean(templateHtml));
|
|
92
|
-
if (!hasContent)
|
|
93
|
-
return;
|
|
94
|
-
this.log('\n Unchanged:');
|
|
95
|
-
if (!changed.subject)
|
|
96
|
-
this.showUnchangedField('Subject', email.subject);
|
|
97
|
-
if (!changed.preview)
|
|
98
|
-
this.showUnchangedField('Preview Text', email.previewText);
|
|
99
|
-
if (!changed.html)
|
|
100
|
-
this.showUnchangedHtml(templateHtml);
|
|
101
|
-
}
|
|
102
|
-
buildDiffPreview(email, updated, templateHtml) {
|
|
103
|
-
const subjectChanged = Boolean(updated.subject) && updated.subject !== email.subject;
|
|
104
|
-
const previewChanged = Boolean(updated.previewText) && updated.previewText !== email.previewText;
|
|
105
|
-
const htmlChanged = Boolean(updated.html) && updated.html !== templateHtml;
|
|
106
|
-
const diff = {};
|
|
107
|
-
diff.subject = subjectChanged
|
|
108
|
-
? { new: updated.subject, old: email.subject }
|
|
109
|
-
: { unchanged: true, value: email.subject };
|
|
110
|
-
if (email.previewText ?? updated.previewText) {
|
|
111
|
-
diff.previewText = previewChanged
|
|
112
|
-
? { new: updated.previewText, old: email.previewText }
|
|
113
|
-
: { unchanged: true, value: email.previewText };
|
|
114
|
-
}
|
|
115
|
-
if (templateHtml ?? updated.html) {
|
|
116
|
-
const oldText = templateHtml
|
|
117
|
-
? this.truncate(this.stripHtml(templateHtml), 500)
|
|
118
|
-
: null;
|
|
119
|
-
const newText = updated.html
|
|
120
|
-
? this.truncate(this.stripHtml(updated.html), 500)
|
|
121
|
-
: null;
|
|
122
|
-
diff.html = htmlChanged
|
|
123
|
-
? { new: newText, old: oldText }
|
|
124
|
-
: { unchanged: true, value: oldText };
|
|
125
|
-
}
|
|
126
|
-
if (updated.ctaText) {
|
|
127
|
-
diff.ctaText = { new: updated.ctaText };
|
|
128
|
-
}
|
|
129
|
-
return { diff };
|
|
130
|
-
}
|
|
131
|
-
showChangeSummary(email, updated, templateHtml) {
|
|
132
|
-
const changed = {
|
|
133
|
-
cta: Boolean(updated.ctaText),
|
|
134
|
-
html: Boolean(updated.html) && updated.html !== templateHtml,
|
|
135
|
-
preview: Boolean(updated.previewText) &&
|
|
136
|
-
updated.previewText !== email.previewText,
|
|
137
|
-
subject: Boolean(updated.subject) && updated.subject !== email.subject,
|
|
138
|
-
};
|
|
139
|
-
this.showSuggestedChanges(email, updated, templateHtml, changed);
|
|
140
|
-
this.showUnchanged(email, templateHtml, changed);
|
|
141
|
-
}
|
|
142
25
|
async run() {
|
|
143
26
|
const { args, flags } = await this.parse(Edit);
|
|
144
27
|
await this.ensureAuth();
|
|
@@ -149,6 +32,9 @@ export default class Edit extends BaseCommand {
|
|
|
149
32
|
}
|
|
150
33
|
const email = yamlConfig.emails[emailIndex];
|
|
151
34
|
const templateHtml = await loadTemplate(`${email.id}.html`);
|
|
35
|
+
if (!flags.json) {
|
|
36
|
+
this.log(`\n Current subject: '${chalk.cyan(email.subject)}'`);
|
|
37
|
+
}
|
|
152
38
|
let changeDescription = flags.change;
|
|
153
39
|
if (!changeDescription) {
|
|
154
40
|
changeDescription = await input({
|
|
@@ -172,11 +58,12 @@ export default class Edit extends BaseCommand {
|
|
|
172
58
|
this.handleApiError(response);
|
|
173
59
|
}
|
|
174
60
|
const updated = response.data;
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
this.
|
|
61
|
+
const oldSubject = email.subject;
|
|
62
|
+
const newSubject = updated.subject || email.subject;
|
|
63
|
+
if (!flags.json && oldSubject !== newSubject) {
|
|
64
|
+
this.log(`\n Suggested subject:`);
|
|
65
|
+
this.log(` ${chalk.red(`- ${oldSubject}`)}`);
|
|
66
|
+
this.log(` ${chalk.green(`+ ${newSubject}`)}`);
|
|
180
67
|
}
|
|
181
68
|
if (!flags.yes) {
|
|
182
69
|
const accepted = await confirm({
|
|
@@ -188,8 +75,6 @@ export default class Edit extends BaseCommand {
|
|
|
188
75
|
return;
|
|
189
76
|
}
|
|
190
77
|
}
|
|
191
|
-
const oldSubject = email.subject;
|
|
192
|
-
const newSubject = updated.subject || email.subject;
|
|
193
78
|
if (updated.subject)
|
|
194
79
|
email.subject = updated.subject;
|
|
195
80
|
if (updated.previewText)
|
|
@@ -206,9 +91,6 @@ export default class Edit extends BaseCommand {
|
|
|
206
91
|
if (flags.json) {
|
|
207
92
|
this.log(JSON.stringify({
|
|
208
93
|
diff: {
|
|
209
|
-
previewText: updated.previewText && updated.previewText !== email.previewText
|
|
210
|
-
? { new: updated.previewText, old: email.previewText }
|
|
211
|
-
: undefined,
|
|
212
94
|
subject: oldSubject === newSubject
|
|
213
95
|
? undefined
|
|
214
96
|
: { new: newSubject, old: oldSubject },
|
|
@@ -89,11 +89,7 @@ export default class Preview extends BaseCommand {
|
|
|
89
89
|
const rendered = templateHtml
|
|
90
90
|
? renderTemplate(templateHtml, sampleData)
|
|
91
91
|
: '';
|
|
92
|
-
await this.sendTestEmail(email, rendered,
|
|
93
|
-
domain: yamlConfig.project?.domain,
|
|
94
|
-
jsonOutput: flags.json,
|
|
95
|
-
toAddress: flags.send,
|
|
96
|
-
});
|
|
92
|
+
await this.sendTestEmail(email, rendered, yamlConfig.project?.domain, flags.send, flags.json);
|
|
97
93
|
return;
|
|
98
94
|
}
|
|
99
95
|
if (flags.text) {
|
|
@@ -130,8 +126,7 @@ export default class Preview extends BaseCommand {
|
|
|
130
126
|
* Calls the API to send a test email to the specified address.
|
|
131
127
|
* Before domain verification, tests send via the mailmodo.com domain.
|
|
132
128
|
*/
|
|
133
|
-
async sendTestEmail(email, html,
|
|
134
|
-
const { domain, jsonOutput, toAddress } = opts;
|
|
129
|
+
async sendTestEmail(email, html, domain, toAddress, jsonOutput) {
|
|
135
130
|
await this.ensureAuth();
|
|
136
131
|
const response = await this.withApiSpinner({ json: jsonOutput, text: ' Sending test email...' }, () => this.apiClient.post(`${API_ENDPOINTS.PREVIEW}/send`, {
|
|
137
132
|
domain,
|
|
@@ -52,7 +52,7 @@ export default class Settings extends BaseCommand {
|
|
|
52
52
|
const key = flags.set.slice(0, eqIndex).trim();
|
|
53
53
|
const propKey = settingKeyToProp(key);
|
|
54
54
|
const value = flags.set.slice(eqIndex + 1).trim();
|
|
55
|
-
if (!(propKey in project)) {
|
|
55
|
+
if (!(propKey in project) && key !== 'logo_file') {
|
|
56
56
|
this.error(`Unknown setting: ${key}`);
|
|
57
57
|
}
|
|
58
58
|
project[propKey] =
|
|
@@ -73,7 +73,11 @@ export default class Settings extends BaseCommand {
|
|
|
73
73
|
const domainVerified = await this.fetchDomainVerified(project.domain);
|
|
74
74
|
this.log(`\n Current settings for ${chalk.bold(project.name || 'project')}:\n`);
|
|
75
75
|
for (const [group, keys] of Object.entries(SETTINGS_GROUPS)) {
|
|
76
|
-
const availableKeys = keys.filter((key) =>
|
|
76
|
+
const availableKeys = keys.filter((key) => {
|
|
77
|
+
if (group === 'brand' && key === 'logo_file')
|
|
78
|
+
return true;
|
|
79
|
+
return settingKeyToProp(key) in project;
|
|
80
|
+
});
|
|
77
81
|
if (availableKeys.length === 0) {
|
|
78
82
|
const hint = SETUP_HINTS[settingKeyToProp(keys[0])];
|
|
79
83
|
if (hint) {
|
|
@@ -98,7 +102,8 @@ export default class Settings extends BaseCommand {
|
|
|
98
102
|
}
|
|
99
103
|
this.log(` ${key.padEnd(16)} ${displayValue}`);
|
|
100
104
|
}
|
|
101
|
-
const missingKeys = keys.filter((key) => !(settingKeyToProp(key) in project)
|
|
105
|
+
const missingKeys = keys.filter((key) => !(settingKeyToProp(key) in project) &&
|
|
106
|
+
!(group === 'brand' && key === 'logo_file'));
|
|
102
107
|
for (const key of missingKeys) {
|
|
103
108
|
const hint = SETUP_HINTS[settingKeyToProp(key)];
|
|
104
109
|
if (hint) {
|
|
@@ -125,6 +130,10 @@ export default class Settings extends BaseCommand {
|
|
|
125
130
|
return;
|
|
126
131
|
const editPropKey = settingKeyToProp(editKey);
|
|
127
132
|
if (!(editPropKey in project)) {
|
|
133
|
+
if (editKey === 'logo_file') {
|
|
134
|
+
await this.handleLogoUpload(yamlConfig);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
128
137
|
const hint = SETUP_HINTS[editPropKey];
|
|
129
138
|
if (hint) {
|
|
130
139
|
this.log(`\n ${editKey} is not configured yet. Run ${chalk.cyan(hint)} to set it up.\n`);
|
|
@@ -252,8 +261,16 @@ export default class Settings extends BaseCommand {
|
|
|
252
261
|
}
|
|
253
262
|
await this.ensureAuth();
|
|
254
263
|
const fileBuffer = await readFile(resolvedPath);
|
|
264
|
+
const ext = resolvedPath.split('.').pop()?.toLowerCase();
|
|
265
|
+
const mimeTypes = {
|
|
266
|
+
png: 'image/png',
|
|
267
|
+
jpg: 'image/jpeg',
|
|
268
|
+
jpeg: 'image/jpeg',
|
|
269
|
+
svg: 'image/svg+xml',
|
|
270
|
+
};
|
|
271
|
+
const mimeType = mimeTypes[ext ?? ''] ?? 'application/octet-stream';
|
|
255
272
|
const formData = new FormData();
|
|
256
|
-
formData.append('logo', new Blob([new Uint8Array(fileBuffer)]), logoPath.split(/[/\\]/).pop() || 'logo.png');
|
|
273
|
+
formData.append('logo', new Blob([new Uint8Array(fileBuffer)], { type: mimeType }), logoPath.split(/[/\\]/).pop() || 'logo.png');
|
|
257
274
|
const response = await this.apiClient.postFormData(API_ENDPOINTS.ASSETS_LOGO, formData);
|
|
258
275
|
if (!response.ok) {
|
|
259
276
|
this.handleApiError(response);
|
package/dist/lib/constants.d.ts
CHANGED
package/dist/lib/constants.js
CHANGED
package/oclif.manifest.json
CHANGED
package/package.json
CHANGED