@@ -21,6 +21,27 @@ const PROCESSED_FOLDER = process.env.PROCESSED_FOLDER || 'Forwarded';
2121const DAEMON = ( process . env . DAEMON || '' ) . toLowerCase ( ) === 'true' ;
2222const POLL_INTERVAL_MS = Number ( process . env . POLL_INTERVAL_MS || 60000 ) ;
2323
24+ const LOG_LEVELS = { error : 0 , warn : 1 , info : 2 , debug : 3 } as const ;
25+ type LogLevel = keyof typeof LOG_LEVELS ;
26+
27+ const configuredLevel = ( process . env . LOG_LEVEL ?. toLowerCase ( ) ?? 'info' ) as LogLevel ;
28+ const currentLogLevel = LOG_LEVELS [ configuredLevel ] ?? LOG_LEVELS . info ;
29+
30+ const logger = {
31+ error : ( msg : string , ...args : any [ ] ) => {
32+ if ( currentLogLevel >= LOG_LEVELS . error ) console . error ( `[ERROR] ${ msg } ` , ...args ) ;
33+ } ,
34+ warn : ( msg : string , ...args : any [ ] ) => {
35+ if ( currentLogLevel >= LOG_LEVELS . warn ) console . warn ( `[WARN] ${ msg } ` , ...args ) ;
36+ } ,
37+ info : ( msg : string , ...args : any [ ] ) => {
38+ if ( currentLogLevel >= LOG_LEVELS . info ) console . log ( `[INFO] ${ msg } ` , ...args ) ;
39+ } ,
40+ debug : ( msg : string , ...args : any [ ] ) => {
41+ if ( currentLogLevel >= LOG_LEVELS . debug ) console . log ( `[DEBUG] ${ msg } ` , ...args ) ;
42+ } ,
43+ } ;
44+
2445function validateEnvironmentVariables ( ) {
2546 const requiredVars = [
2647 "IMAP_HOST" ,
@@ -38,45 +59,20 @@ function validateEnvironmentVariables() {
3859 const missingVars = requiredVars . filter ( ( varName ) => ! process . env [ varName ] ) ;
3960
4061 if ( missingVars . length > 0 ) {
41- console . error (
42- "Missing required environment variables:" ,
43- missingVars . join ( ", " )
44- ) ;
62+ logger . error ( "Missing required environment variables: " + missingVars . join ( ", " ) ) ;
4563 process . exit ( 1 ) ;
4664 }
4765
48- console . log ( "All required environment variables are set." ) ;
49-
50- // Log domain filtering configuration
66+ logger . debug ( "All required environment variables are set." ) ;
67+
5168 if ( ALLOWED_SENDER_DOMAINS ) {
52- console . log ( "Domain filtering enabled. Allowed domains:" , ALLOWED_SENDER_DOMAINS ) ;
69+ logger . info ( "Domain filtering enabled. Allowed domains: " + ALLOWED_SENDER_DOMAINS ) ;
5370 } else {
54- console . log ( "Domain filtering disabled. All emails will be forwarded." ) ;
71+ logger . debug ( "Domain filtering disabled. All emails will be forwarded." ) ;
5572 }
5673}
5774
58- // Custom logger for ImapFlow using console statements
59- const customLogger = {
60- trace : ( ) => { } , // Suppress trace logs
61- debug : ( ) => { } , // Suppress debug logs
62- info : ( msg : any , ...args : any [ ] ) => {
63- // Only log important info messages
64- if ( typeof msg === 'string' && ( msg . includes ( 'Connected' ) || msg . includes ( 'Authenticated' ) ) ) {
65- console . log ( 'IMAP:' , msg , ...args ) ;
66- }
67- } ,
68- warn : ( msg : any , ...args : any [ ] ) => {
69- console . warn ( 'IMAP Warning:' , msg , ...args ) ;
70- } ,
71- error : ( msg : any , ...args : any [ ] ) => {
72- console . error ( 'IMAP Error:' , msg , ...args ) ;
73- } ,
74- fatal : ( msg : any , ...args : any [ ] ) => {
75- console . error ( 'IMAP Fatal:' , msg , ...args ) ;
76- }
77- } ;
78-
79- function shouldForwardEmail ( email ) {
75+ function shouldForwardEmail ( email : import ( 'mailparser' ) . ParsedMail ) {
8076 // If no domain filtering is configured, forward all emails
8177 if ( ! ALLOWED_SENDER_DOMAINS ) {
8278 return true ;
@@ -86,15 +82,15 @@ function shouldForwardEmail(email) {
8682 const fromAddress = email . from ?. value ?. [ 0 ] ?. address || email . from ?. text || '' ;
8783
8884 if ( ! fromAddress ) {
89- console . log ( "No sender address found, skipping email" ) ;
85+ logger . debug ( "No sender address found, skipping email" ) ;
9086 return false ;
9187 }
9288
9389 // Extract domain from sender email
9490 const senderDomain = fromAddress . split ( '@' ) [ 1 ] ?. toLowerCase ( ) ;
95-
91+
9692 if ( ! senderDomain ) {
97- console . log ( " Invalid sender email format:" , fromAddress ) ;
93+ logger . warn ( ` Invalid sender email format: ${ fromAddress } ` ) ;
9894 return false ;
9995 }
10096
@@ -105,15 +101,22 @@ function shouldForwardEmail(email) {
105101 . filter ( domain => domain . length > 0 ) ;
106102
107103 const isAllowed = allowedDomains . includes ( senderDomain ) ;
108-
104+
109105 if ( ! isAllowed ) {
110- console . log ( `Email from ${ fromAddress } ( domain: ${ senderDomain } ) not in allowed domains : ${ allowedDomains . join ( ', ' ) } `) ;
106+ logger . info ( `Skipping email from ${ fromAddress } — domain not in allowlist : ${ allowedDomains . join ( ', ' ) } `) ;
111107 }
112108
113109 return isAllowed ;
114110}
115111
116- async function forwardMail ( email ) {
112+ interface MailToForward {
113+ subject : string ;
114+ text : string ;
115+ html : string | undefined ;
116+ attachments : { filename : string | undefined ; content : Buffer } [ ] ;
117+ }
118+
119+ async function forwardMail ( email : MailToForward ) {
117120 const transporter = nodemailer . createTransport ( {
118121 host : SMTP_HOST ! ,
119122 port : parseInt ( SMTP_PORT ! , 10 ) ,
@@ -135,160 +138,137 @@ async function forwardMail(email) {
135138
136139 try {
137140 await transporter . sendMail ( mailOptions ) ;
138- console . log ( " Forwarded email:" , email . subject ) ;
141+ logger . info ( ` Forwarded email: ${ email . subject } ` ) ;
139142 } catch ( error ) {
140- console . error ( " Failed to forward email:" , email . subject , " Error:" , error . message ) ;
143+ logger . error ( ` Failed to forward email: ${ email . subject } — ${ error instanceof Error ? error . message : error } ` ) ;
141144 throw error ; // Re-throw to let the caller decide how to handle
142145 }
143146}
144147
145148async function processUnseen ( client : ImapFlow ) {
146- console . log ( "Starting to process unseen messages...") ;
147-
149+ logger . debug ( "Checking for unseen messages...") ;
150+
148151 // Ensure INBOX is open
149152 if ( ! client . mailbox || client . mailbox . path !== 'INBOX' ) {
150- console . log ( "Opening INBOX..." ) ;
153+ logger . debug ( "Opening INBOX..." ) ;
151154 await client . mailboxOpen ( 'INBOX' ) ;
152155 }
153156
154- console . log ( "Searching for unseen messages..." ) ;
155157 // Fetch all unseen at once to avoid deadlocks
156158 const messages = await client . fetchAll ( { seen : false } , { uid : true , envelope : true , source : true } ) ;
157-
158- console . log ( `Found ${ messages . length } unseen messages` ) ;
159159
160160 if ( messages . length === 0 ) {
161- console . log ( "No unseen messages to process" ) ;
161+ logger . debug ( "No unseen messages to process" ) ;
162162 return ;
163163 }
164164
165+ logger . info ( `Found ${ messages . length } unseen message(s)` ) ;
166+
165167 for ( const msg of messages ) {
166- console . log ( `Processing message UID: ${ msg . uid } ` ) ;
168+ logger . debug ( `Processing message UID: ${ msg . uid } ` ) ;
167169 try {
168170 const parsed = await simpleParser ( msg . source as Buffer ) ;
169- console . log ( `Parsed email with subject: ${ parsed . subject || '(no subject)' } ` ) ;
171+ logger . debug ( `Parsed subject: ${ parsed . subject || '(no subject)' } ` ) ;
170172
171173 if ( ! shouldForwardEmail ( parsed ) ) {
172- console . log ( "Skipping email due to domain filter. Marking as seen..." ) ;
174+ logger . debug ( `Domain filter: marking UID ${ msg . uid } as seen and skipping` ) ;
173175 await client . messageFlagsAdd ( { uid : msg . uid } , [ '\\Seen' ] ) ;
174- console . log ( "Email marked as seen successfully" ) ;
175176 continue ;
176177 }
177178
178- console . log ( "Email passed domain filter, forwarding..." ) ;
179179 await forwardMail ( {
180180 subject : parsed . subject || '(no subject)' ,
181181 text : parsed . text || '' ,
182- html : parsed . html || undefined ,
183- attachments : ( parsed . attachments || [ ] ) . map ( a => ( { filename : a . filename , content : a . content } ) ) ,
182+ html : parsed . html || undefined , // normalize false/null → undefined
183+ attachments : ( parsed . attachments || [ ] ) . map ( ( a : import ( 'mailparser' ) . Attachment ) => ( { filename : a . filename , content : a . content } ) ) ,
184184 } ) ;
185185
186186 // Mark as seen immediately so a failed move doesn't cause re-processing
187187 await client . messageFlagsAdd ( { uid : msg . uid } , [ '\\Seen' ] , { uid : true } ) ;
188188
189- console . log ( `Moving email to ${ PROCESSED_FOLDER } ...` ) ;
189+ logger . debug ( `Moving UID ${ msg . uid } to ${ PROCESSED_FOLDER } ...` ) ;
190190 await client . messageMove ( { uid : msg . uid } , PROCESSED_FOLDER , { uid : true } ) ;
191- console . log ( "Email moved successfully" ) ;
192191 } catch ( err ) {
193- console . error ( ' Processing failed for UID' , msg . uid , err ) ;
192+ logger . error ( ` Processing failed for UID ${ msg . uid } : ${ err } ` ) ;
194193 // Leave message untouched for retry
195194 }
196195 }
197-
198- console . log ( "Finished processing all unseen messages" ) ;
199196}
200197
201198async function main ( ) {
202- console . log ( "Starting mail forwarder application..." ) ;
203-
204- // Validate all required environment variables are set
199+ logger . info ( "Starting mail forwarder..." ) ;
200+
205201 validateEnvironmentVariables ( ) ;
206202
207- console . log ( "Creating IMAP client..." ) ;
208203 const client = new ImapFlow ( {
209204 host : IMAP_HOST ! ,
210205 port : parseInt ( IMAP_PORT ! , 10 ) ,
211206 secure : true ,
212207 auth : { user : IMAP_USER ! , pass : IMAP_PASSWORD ! } ,
213- logger : false // Disable ImapFlow logging completely
208+ logger : false ,
214209 } ) ;
215210
216211 try {
217- console . log ( "Connecting to IMAP server..." ) ;
212+ logger . debug ( "Connecting to IMAP server..." ) ;
218213 await client . connect ( ) ;
219- console . log ( "Connected successfully " ) ;
220-
221- console . log ( "Opening INBOX..." ) ;
214+ logger . info ( "Connected to IMAP server " ) ;
215+
216+ logger . debug ( "Opening INBOX..." ) ;
222217 await client . mailboxOpen ( 'INBOX' ) ;
223- console . log ( "INBOX opened successfully" ) ;
224218
225219 // Ensure processed folder exists
226220 const existing = await client . list ( ) ;
227221 if ( ! existing . some ( m => m . path === PROCESSED_FOLDER ) ) {
228- console . log ( `Creating folder: ${ PROCESSED_FOLDER } ` ) ;
222+ logger . info ( `Creating folder: ${ PROCESSED_FOLDER } ` ) ;
229223 await client . mailboxCreate ( PROCESSED_FOLDER ) ;
230224 }
231225
232- // Initial batch
233- console . log ( "Starting initial message processing..." ) ;
234226 await processUnseen ( client ) ;
235- console . log ( "Initial processing complete" ) ;
236227
237228 if ( DAEMON ) {
238- console . log ( `Daemon mode enabled. Polling every ${ POLL_INTERVAL_MS } ms` ) ;
229+ logger . info ( `Daemon mode: polling every ${ POLL_INTERVAL_MS } ms` ) ;
239230 let busy = false ;
240231
241232 const poll = async ( ) => {
242233 if ( busy ) {
243- console . log ( "Previous poll still running, skipping... " ) ;
234+ logger . debug ( "Previous poll still running, skipping" ) ;
244235 return ;
245236 }
246237 busy = true ;
247- console . log ( "Starting scheduled poll..." ) ;
248- try {
249- await processUnseen ( client ) ;
250- console . log ( "Scheduled poll complete" ) ;
251- }
252- catch ( e ) {
253- console . error ( 'Polling error' , e ) ;
254- }
255- finally {
256- busy = false ;
238+ try {
239+ await processUnseen ( client ) ;
240+ } catch ( e ) {
241+ logger . error ( `Polling error: ${ e } ` ) ;
242+ } finally {
243+ busy = false ;
257244 }
258245 } ;
259246
260247 const timer = setInterval ( poll , POLL_INTERVAL_MS ) ;
261248
262249 const shutdown = async ( code = 0 ) => {
263- console . log ( "Shutting down daemon ..." ) ;
250+ logger . info ( "Shutting down..." ) ;
264251 clearInterval ( timer ) ;
265252 try { await client . logout ( ) ; } catch { }
266253 process . exit ( code ) ;
267254 } ;
268255 process . on ( 'SIGTERM' , ( ) => shutdown ( 0 ) ) ;
269256 process . on ( 'SIGINT' , ( ) => shutdown ( 0 ) ) ;
270257
271- // Keep process alive
272- console . log ( "Daemon running. Press Ctrl+C to stop." ) ;
273- await new Promise ( ( ) => { } ) ; // This keeps the process alive indefinitely
258+ logger . info ( "Daemon running. Press Ctrl+C to stop." ) ;
259+ await new Promise ( ( ) => { } ) ; // Keep process alive
274260 } else {
275- console . log ( "Non-daemon mode, logging out..." ) ;
276261 await client . logout ( ) ;
277- console . log ( "Logged out, exiting..." ) ;
278262 process . exit ( 0 ) ;
279263 }
280264 } catch ( error ) {
281- console . error ( "Error in main function:" , error ) ;
282- try {
283- await client . logout ( ) ;
284- } catch ( logoutError ) {
285- console . error ( "Error during logout:" , logoutError ) ;
286- }
265+ logger . error ( `Fatal error: ${ error } ` ) ;
266+ try { await client . logout ( ) ; } catch { }
287267 process . exit ( 1 ) ;
288268 }
289269}
290270
291271main ( ) . catch ( err => {
292- console . error ( ' Unexpected error in main:' , err ) ;
272+ logger . error ( ` Unexpected error: ${ err } ` ) ;
293273 process . exit ( 1 ) ;
294274} ) ;
0 commit comments