imap-trigger.js 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. /**
  2. * @node imap-trigger
  3. * @name IMAP Email Trigger
  4. * @category email
  5. * @version 1.3.0
  6. * @description Triggers when new emails are received. Polls the IMAP server for new messages.
  7. * @trigger
  8. * @scheduled
  9. * @icon mail
  10. */
  11. const configSchema = {
  12. type: 'object',
  13. properties: {
  14. credentialId: {
  15. type: 'string',
  16. title: 'IMAP Credential',
  17. description: 'Select the IMAP credential to use for authentication',
  18. dynamicOptions: {
  19. source: 'credentials',
  20. filter: { type: 'imap' }
  21. }
  22. },
  23. pollInterval: {
  24. type: 'integer',
  25. title: 'Poll Interval (minutes)',
  26. description: 'How often to check for new emails when workflow is active. Set to 0 for manual-only execution.',
  27. default: 5,
  28. minimum: 0,
  29. maximum: 1440
  30. },
  31. mailbox: {
  32. type: 'string',
  33. title: 'Mailbox',
  34. description: 'The mailbox to monitor (e.g., INBOX)',
  35. default: 'INBOX'
  36. },
  37. filterUnread: {
  38. type: 'boolean',
  39. title: 'Only Unread',
  40. description: 'Only trigger on unread (unseen) emails',
  41. default: true
  42. },
  43. filterFrom: {
  44. type: 'string',
  45. title: 'Filter by From',
  46. description: 'Only trigger on emails from this address (optional)'
  47. },
  48. filterSubject: {
  49. type: 'string',
  50. title: 'Filter by Subject',
  51. description: 'Only trigger on emails containing this text in subject (optional)'
  52. },
  53. markAsRead: {
  54. type: 'boolean',
  55. title: 'Mark as Read',
  56. description: 'Mark emails as read after processing',
  57. default: true
  58. },
  59. limit: {
  60. type: 'integer',
  61. title: 'Max Emails',
  62. description: 'Maximum number of emails to process per trigger',
  63. default: 10,
  64. minimum: 1,
  65. maximum: 100
  66. },
  67. fetchContent: {
  68. type: 'boolean',
  69. title: 'Fetch Full Content',
  70. description: 'Fetch full email content including body and raw data (required for attachment extraction)',
  71. default: true
  72. }
  73. },
  74. required: ['credentialId']
  75. };
  76. const outputSchema = {
  77. type: 'object',
  78. properties: {
  79. emails: {
  80. type: 'array',
  81. items: {
  82. type: 'object',
  83. properties: {
  84. uid: { type: 'string', description: 'Unique email identifier' },
  85. subject: { type: 'string', description: 'Decoded - RFC 2047 encoded words are resolved by the IMAP client' },
  86. from: { type: 'string', description: 'Decoded, in "Name <address>" form' },
  87. to: { type: 'string', description: 'Decoded, in "Name <address>" form' },
  88. cc: { type: 'string', description: 'Decoded, in "Name <address>" form. Empty when the message carries no Cc' },
  89. date: { type: 'string' },
  90. body: { type: 'string', description: 'Email body (if fetchContent enabled)' },
  91. raw: { type: 'string', description: 'Raw email content (if fetchContent enabled)' },
  92. hasAttachments: { type: 'boolean', description: 'Whether email has attachments' }
  93. }
  94. }
  95. },
  96. uids: {
  97. type: 'array',
  98. items: { type: 'string' },
  99. description: 'Array of message UIDs'
  100. },
  101. count: { type: 'integer' },
  102. mailbox: { type: 'string' },
  103. timestamp: { type: 'number' }
  104. }
  105. };
  106. const outputs = [
  107. { name: 'main', displayName: 'Email Received', type: 'object', color: '#22c55e' }
  108. ];
  109. /**
  110. * Check if email has attachments by looking for Content-Disposition: attachment
  111. * or multipart content types
  112. */
  113. function detectAttachments(rawEmail) {
  114. if (!rawEmail) return false;
  115. const lower = rawEmail.toLowerCase();
  116. return lower.includes('content-disposition: attachment') ||
  117. lower.includes('content-disposition:attachment') ||
  118. (lower.includes('multipart/mixed') && lower.includes('boundary='));
  119. }
  120. module.exports = {
  121. configSchema,
  122. outputSchema,
  123. outputs,
  124. async execute(config, input, context) {
  125. const {
  126. credentialId,
  127. mailbox = 'INBOX',
  128. filterUnread = true,
  129. filterFrom,
  130. filterSubject,
  131. markAsRead = true,
  132. limit = 10,
  133. fetchContent = true
  134. } = config;
  135. if (!credentialId) {
  136. throw new Error('IMAP credential is required');
  137. }
  138. // Build search criteria
  139. let criteria = filterUnread ? 'UNSEEN' : 'ALL';
  140. if (filterFrom) {
  141. criteria += ` FROM "${filterFrom}"`;
  142. }
  143. if (filterSubject) {
  144. criteria += ` SUBJECT "${filterSubject}"`;
  145. }
  146. // Search for emails
  147. const searchResult = smartbotic.imap.search({
  148. credentialId,
  149. mailbox,
  150. criteria,
  151. limit
  152. });
  153. if (!searchResult.success) {
  154. throw new Error(`IMAP search failed: ${searchResult.error}`);
  155. }
  156. const emails = [];
  157. const uids = searchResult.uids || [];
  158. // Fetch each email if fetchContent is enabled
  159. if (fetchContent) {
  160. for (const uid of uids) {
  161. // Use peek: true to avoid marking as read during fetch
  162. // Only mark as read explicitly if markAsRead is enabled
  163. const fetchResult = smartbotic.imap.fetch({
  164. credentialId,
  165. mailbox,
  166. uid,
  167. peek: !markAsRead // peek=true prevents marking as read
  168. });
  169. if (fetchResult.success) {
  170. const email = {
  171. // Spread rather than a field-by-field copy - naming them
  172. // is how cc was silently dropped when the IMAP client
  173. // started returning it.
  174. ...(({ success, ...rest }) => rest)(fetchResult),
  175. hasAttachments: detectAttachments(fetchResult.raw)
  176. };
  177. emails.push(email);
  178. }
  179. }
  180. } else {
  181. // Just return UIDs without fetching content
  182. for (const uid of uids) {
  183. emails.push({ uid });
  184. // Mark as read if configured (when not fetching content)
  185. if (markAsRead) {
  186. smartbotic.imap.modify({
  187. credentialId,
  188. mailbox,
  189. uid,
  190. action: 'markRead'
  191. });
  192. }
  193. }
  194. }
  195. return {
  196. emails,
  197. uids,
  198. count: emails.length,
  199. mailbox,
  200. timestamp: Date.now()
  201. };
  202. }
  203. };