@@ -79,6 +79,70 @@ export interface OutlookWebhookPayload {
7979 rawEmail ?: OutlookEmail // Only included when includeRawEmail is true
8080}
8181
82+ /**
83+ * Convert HTML content to a readable plain-text representation.
84+ * Keeps reasonable newlines and decodes common HTML entities.
85+ */
86+ function convertHtmlToPlainText ( html : string ) : string {
87+ if ( ! html ) return ''
88+
89+ let working = html
90+
91+ // Remove script and style content
92+ working = working . replace ( / < s c r i p t [ \s \S ] * ?> [ \s \S ] * ?< \/ s c r i p t > / gi, '' )
93+ working = working . replace ( / < s t y l e [ \s \S ] * ?> [ \s \S ] * ?< \/ s t y l e > / gi, '' )
94+
95+ // Line breaks for common block-level tags
96+ working = working
97+ . replace ( / < b r \s * \/ ? > (? = \s | $ ) / gi, '\n' )
98+ . replace ( / < \/ ( p | d i v | l i | h [ 1 - 6 ] | t r ) > / gi, '\n' )
99+ . replace ( / < ( p | d i v | l i | h [ 1 - 6 ] | t r ) [ ^ > ] * > / gi, '' )
100+ . replace ( / < \/ ( t d | t h ) > / gi, '\t' )
101+
102+ // Remove all remaining tags
103+ working = working . replace ( / < [ ^ > ] + > / g, '' )
104+
105+ // Decode common HTML entities
106+ const entityMap : Record < string , string > = {
107+ ' ' : ' ' ,
108+ '&' : '&' ,
109+ '<' : '<' ,
110+ '>' : '>' ,
111+ '"' : '"' ,
112+ ''' : "'" ,
113+ }
114+ for ( const [ entity , char ] of Object . entries ( entityMap ) ) {
115+ working = working . split ( entity ) . join ( char )
116+ }
117+ // Numeric entities (decimal)
118+ working = working . replace ( / & # ( \d + ) ; / g, ( _ , dec : string ) => {
119+ const code = Number ( dec )
120+ return Number . isFinite ( code ) ? String . fromCharCode ( code ) : _
121+ } )
122+ // Numeric entities (hex)
123+ working = working . replace ( / & # x ( [ 0 - 9 a - f A - F ] + ) ; / g, ( _ , hex : string ) => {
124+ const code = Number . parseInt ( hex , 16 )
125+ return Number . isFinite ( code ) ? String . fromCharCode ( code ) : _
126+ } )
127+
128+ // Normalize whitespace
129+ working = working
130+ . replace ( / \r \n / g, '\n' )
131+ . replace ( / \r / g, '\n' )
132+ . replace ( / \t / g, ' ' )
133+ . replace ( / \u00A0 / g, ' ' )
134+
135+ // Collapse excessive blank lines and trim
136+ working = working
137+ . split ( '\n' )
138+ . map ( ( line ) => line . trimEnd ( ) )
139+ . join ( '\n' )
140+ . replace ( / \n { 3 , } / g, '\n\n' )
141+ . trim ( )
142+
143+ return working
144+ }
145+
82146export async function pollOutlookWebhooks ( ) {
83147 logger . info ( 'Starting Outlook webhook polling' )
84148
@@ -357,7 +421,18 @@ async function processOutlookEmails(
357421 to : email . toRecipients ?. map ( ( r ) => r . emailAddress . address ) . join ( ', ' ) || '' ,
358422 cc : email . ccRecipients ?. map ( ( r ) => r . emailAddress . address ) . join ( ', ' ) || '' ,
359423 date : email . receivedDateTime ,
360- bodyText : email . bodyPreview || '' ,
424+ bodyText : ( ( ) => {
425+ const content = email . body ?. content || ''
426+ const type = ( email . body ?. contentType || '' ) . toLowerCase ( )
427+ if ( ! content ) {
428+ return email . bodyPreview || ''
429+ }
430+ if ( type === 'text' || type === 'text/plain' ) {
431+ return content
432+ }
433+ // Default to converting HTML or unknown types
434+ return convertHtmlToPlainText ( content )
435+ } ) ( ) ,
361436 bodyHtml : email . body ?. content || '' ,
362437 hasAttachments : email . hasAttachments ,
363438 isRead : email . isRead ,
0 commit comments