| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220 |
- /**
- * @node smtp-send
- * @name Send Email
- * @category email
- * @version 1.0.0
- * @description Send an email through an SMTP server, with optional attachments taken from the file store
- * @icon send
- */
- const configSchema = {
- type: 'object',
- properties: {
- credentialId: {
- type: 'string',
- title: 'Mail Credential',
- description: 'An SMTP credential, or the IMAP credential for the same mailbox',
- dynamicOptions: {
- source: 'credentials',
- filter: { type: ['smtp', 'imap'] }
- }
- },
- to: {
- type: 'string',
- title: 'To',
- description: 'Recipient address, or several separated by commas'
- },
- cc: {
- type: 'string',
- title: 'Cc',
- description: 'Copied recipients, separated by commas'
- },
- bcc: {
- type: 'string',
- title: 'Bcc',
- description: 'Blind copied recipients, separated by commas'
- },
- subject: {
- type: 'string',
- title: 'Subject'
- },
- body: {
- type: 'string',
- title: 'Body',
- description: 'Message text. Supports {{variable}} interpolation.'
- },
- html: {
- type: 'boolean',
- title: 'Send as HTML',
- description: 'Send the body as HTML rather than plain text',
- default: false
- },
- replyTo: {
- type: 'string',
- title: 'Reply-To',
- description: 'Address replies should go to, when it differs from the sender'
- },
- fromAddress: {
- type: 'string',
- title: 'From',
- description: 'Sender address. Defaults to the one on the credential.'
- },
- fromName: {
- type: 'string',
- title: 'From Name',
- description: 'Name shown to recipients alongside the address'
- },
- attachmentFileIds: {
- type: 'string',
- title: 'Attachment File IDs',
- description: 'File store ids to attach, separated by commas'
- }
- },
- required: ['credentialId', 'to']
- };
- const inputSchema = {
- type: 'object',
- properties: {
- to: { type: 'string', description: 'Overrides the configured recipients' },
- subject: { type: 'string', description: 'Overrides the configured subject' },
- body: { type: 'string', description: 'Overrides the configured body' },
- attachments: {
- type: 'array',
- description: 'Attachments supplied by an earlier node',
- items: {
- type: 'object',
- properties: {
- filename: { type: 'string' },
- mimeType: { type: 'string' },
- contentBase64: { type: 'string', description: 'Attachment content, base64 encoded' },
- fileId: { type: 'string', description: 'File store id to read the content from instead' }
- }
- }
- }
- }
- };
- const outputSchema = {
- type: 'object',
- properties: {
- success: { type: 'boolean' },
- messageId: { type: 'string', description: 'The Message-ID the mail was sent with' },
- accepted: { type: 'integer', description: 'How many addresses the message was submitted for' },
- to: { type: 'array', items: { type: 'string' } },
- subject: { type: 'string' }
- }
- };
- const outputs = [
- { name: 'main', displayName: 'Sent', type: 'object', color: '#3b82f6' }
- ];
- // A connection hands the previous node's whole output over as input.data, and
- // some nodes wrap their own return in a result object on top of that. Rather
- // than guess which shape arrived, the layers are flattened with the innermost
- // winning, so a field set anywhere in the chain is found.
- function incoming(input) {
- const isPlain = (v) => v && typeof v === 'object' && !Array.isArray(v);
- const merged = {};
- for (const layer of [input, input && input.data, input && input.data && input.data.result]) {
- if (!isPlain(layer)) continue;
- for (const key of Object.keys(layer)) {
- merged[key] = layer[key];
- }
- }
- return merged;
- }
- // Addresses are written the way people write them - one line, commas between -
- // so the node splits rather than asking for an array in the form.
- function splitAddresses(value) {
- if (!value) return [];
- if (Array.isArray(value)) return value.map(String).map(a => a.trim()).filter(Boolean);
- return String(value).split(',').map(a => a.trim()).filter(Boolean);
- }
- // An attachment is either carried inline by the previous node or left in the
- // file store; a file id means the bytes have to be fetched before sending.
- function resolveAttachment(entry) {
- if (entry.contentBase64) {
- return {
- filename: entry.filename || 'attachment',
- mimeType: entry.mimeType || '',
- contentBase64: entry.contentBase64
- };
- }
- if (!entry.fileId) return null;
- const info = smartbotic.storage.getFileInfo(entry.fileId);
- const file = smartbotic.storage.downloadFile(entry.fileId);
- if (!file || !file.success || !file.data) return null;
- return {
- filename: entry.filename || (info && info.name) || entry.fileId,
- mimeType: entry.mimeType || (info && info.mimeType) || '',
- contentBase64: file.data
- };
- }
- module.exports = {
- configSchema,
- inputSchema,
- outputSchema,
- outputs,
- async execute(config, input, context) {
- const from = incoming(input);
- if (!config.credentialId) {
- throw new Error('A mail credential is required');
- }
- const to = splitAddresses(from.to || config.to);
- if (to.length === 0) {
- throw new Error('At least one recipient is required');
- }
- const subject = from.subject || config.subject || '';
- const body = from.body || config.body || '';
- const attachments = [];
- for (const entry of (from.attachments || [])) {
- const resolved = resolveAttachment(entry);
- if (resolved) attachments.push(resolved);
- }
- for (const fileId of splitAddresses(config.attachmentFileIds)) {
- const resolved = resolveAttachment({ fileId });
- if (resolved) attachments.push(resolved);
- }
- const result = smartbotic.smtp.send({
- credentialId: config.credentialId,
- to,
- cc: splitAddresses(config.cc),
- bcc: splitAddresses(config.bcc),
- subject,
- body,
- html: config.html === true,
- replyTo: config.replyTo || '',
- from: config.fromAddress || '',
- fromName: config.fromName || '',
- attachments
- });
- smartbotic.log.info('Sent "' + subject + '" to ' + to.join(', '));
- return {
- success: true,
- messageId: result.messageId,
- accepted: result.accepted,
- to,
- subject
- };
- }
- };
|