imap-search.js 7.3 KB

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