1+ /**
2+ * Allow external resources for the provided element.
3+ *
4+ * @param {HTMLElement } element - The element containing the allow button.
5+ * @param {string } messagePart - The message part associated with the resource.
6+ * * @param {Boolean } inline - true if the message is displayed in inline mode, false otherwise.
7+ * @returns {void }
8+ */
9+ function handleAllowResource ( element , messagePart , inline = false ) {
10+ element . querySelector ( 'a' ) . addEventListener ( 'click' , function ( e ) {
11+ e . preventDefault ( ) ;
12+ $ ( '.msg_text_inner' ) . remove ( ) ;
13+ const externalSources = $ ( this ) . data ( 'src' ) . split ( ',' ) ;
14+ externalSources ?. forEach ( ( source ) => Hm_Utils . save_to_local_storage ( source , 1 ) ) ;
15+ if ( inline ) {
16+ return inline_imap_msg ( window . inline_msg_details , window . inline_msg_uid ) ;
17+ }
18+ return get_message_content ( getParam ( 'part' ) , getMessageUidParam ( ) , getListPathParam ( ) , getParam ( 'list_parent' ) , false , false , false ) ;
19+ } ) ;
20+ }
21+
22+ /**
23+ * Create and insert in the DOM an element containing a message and a button to allow the resource.
24+ *
25+ * @param {HTMLElement } element - The element having the blocked resource.
26+ * @param {Boolean } inline - true if the message is displayed in inline mode, false otherwise.
27+ * @returns {void }
28+ */
29+ function handleInvisibleResource ( element , inline = false ) {
30+ const dataSrc = element . dataset . src ;
31+
32+ const allowResource = document . createElement ( 'div' ) ;
33+ allowResource . classList . add ( 'alert' , 'alert-warning' , 'p-1' ) ;
34+
35+ const source = dataSrc . substring ( 0 , 40 ) + ( dataSrc . length > 40 ? '...' : '' ) ;
36+ allowResource . innerHTML = `Source blocked: ${ element . alt ? element . alt : source }
37+ <a href="#" data-src="${ dataSrc } " class="btn btn-light btn-sm">
38+ Allow</a></div>
39+ ` ;
40+
41+ document . querySelector ( '.external_notices' ) . insertAdjacentElement ( 'beforeend' , allowResource ) ;
42+ handleAllowResource ( allowResource , element . dataset . messagePart , inline ) ;
43+ }
44+
45+ const handleExternalResources = ( inline ) => {
46+ const messageContainer = document . querySelector ( '.msg_text_inner' ) ;
47+ const externalNoticesAccordion = document . createElement ( 'div' ) ;
48+ externalNoticesAccordion . classList . add ( 'accordion' ) ;
49+ externalNoticesAccordion . id = 'externalNoticesAccordion' ;
50+ messageContainer . insertAdjacentElement ( 'afterbegin' , externalNoticesAccordion ) ;
51+ externalNoticesAccordion . innerHTML = '<div class="external_notices accordion-collapse collapse"></div>' ;
52+
53+ const senderEmail = document . querySelector ( '#contact_info' ) ?. textContent . match ( EMAIL_REGEX ) [ 0 ] ;
54+
55+ if ( handleBlockedStatus ( inline , senderEmail ) ) {
56+ return ;
57+ }
58+
59+ const sender = senderEmail + '_external_resources_allowed' ;
60+ const elements = messageContainer . querySelectorAll ( '[data-src]' ) ;
61+ const blockedResources = [ ] ;
62+
63+ elements . forEach ( function ( element ) {
64+
65+ const dataSrc = element . dataset . src ;
66+ const senderAllowed = Hm_Utils . get_from_local_storage ( sender ) ;
67+ const allowed = Hm_Utils . get_from_local_storage ( dataSrc ) ;
68+
69+ switch ( Number ( allowed ) || Number ( senderAllowed ) ) {
70+ case 1 :
71+ element . src = dataSrc ;
72+ break ;
73+ default :
74+ if ( ( allowed || senderAllowed ) === null ) {
75+ Hm_Utils . save_to_local_storage ( dataSrc , 0 ) ;
76+ }
77+ handleInvisibleResource ( element , inline ) ;
78+ blockedResources . push ( dataSrc ) ;
79+ break ;
80+ }
81+ } ) ;
82+
83+ const noticesElement = document . createElement ( 'div' ) ;
84+ noticesElement . classList . add ( 'notices' ) ;
85+
86+ if ( blockedResources . length ) {
87+ const allowAll = document . createElement ( 'div' ) ;
88+ allowAll . classList . add ( 'allow_image_link' , 'all' , 'fw-bold' ) ;
89+ allowAll . textContent = 'For security reasons, external resources have been blocked.' ;
90+ if ( blockedResources . length > 1 ) {
91+ const allowAllLink = document . createElement ( 'a' ) ;
92+ allowAllLink . classList . add ( 'btn' , 'btn-light' , 'btn-sm' ) ;
93+ allowAllLink . href = '#' ;
94+ allowAllLink . dataset . src = blockedResources . join ( ',' ) ;
95+ allowAllLink . textContent = 'Allow all' ;
96+
97+ const expandLink = document . createElement ( 'a' ) ;
98+ expandLink . classList . add ( 'btn' , 'btn-sm' , 'd-flex' , 'align-items-center' , 'gap-2' ) ;
99+ expandLink . href = '#' ;
100+ expandLink . setAttribute ( 'data-bs-toggle' , 'collapse' ) ;
101+ expandLink . setAttribute ( 'data-bs-target' , '.external_notices' ) ;
102+ expandLink . innerHTML = 'Show details <i class="bi bi-chevron-down"></i>' ;
103+ document . querySelector ( '.external_notices' ) . addEventListener ( 'show.bs.collapse' , function ( ) {
104+ expandLink . innerHTML = 'Hide details<i class="bi bi-chevron-up"></i>' ;
105+ } ) ;
106+ document . querySelector ( '.external_notices' ) . addEventListener ( 'hide.bs.collapse' , function ( ) {
107+ expandLink . innerHTML = 'Show details<i class="bi bi-chevron-down"></i>' ;
108+ } ) ;
109+
110+ const linksWrapper = $ ( '<div class="d-inline-flex"></div>' ) ;
111+ linksWrapper . append ( allowAllLink ) ;
112+ linksWrapper . append ( expandLink ) ;
113+ allowAll . appendChild ( linksWrapper [ 0 ] ) ;
114+
115+ handleAllowResource ( allowAll , getParam ( 'part' ) , inline ) ;
116+ }
117+ noticesElement . insertAdjacentElement ( 'afterbegin' , allowAll ) ;
118+
119+ const definitiveActions = $ ( '<div class="definitive_actions ms-auto">From this sender always:</div>' ) ;
120+
121+ const button = document . createElement ( 'a' ) ;
122+ button . setAttribute ( 'href' , '#' ) ;
123+ button . classList . add ( 'always_allow_image' , 'btn' , 'btn-light' , 'btn-sm' ) ;
124+ button . innerHTML = '<i class="bi bi-check"></i> Allow' ;
125+ definitiveActions . append ( button ) ;
126+ const popover = sessionAvailableOnlyActionInfo ( button )
127+
128+ button . addEventListener ( 'click' , function ( e ) {
129+ e . preventDefault ( ) ;
130+ addSenderToImagesWhitelist ( senderEmail ) . then ( refreshMessageContent . bind ( null , inline ) ) . finally ( ( ) => {
131+ popover . dispose ( ) ;
132+ } )
133+ } ) ;
134+
135+ const alwaysBlockButton = $ ( '<a href="#" class="btn btn-light btn-sm ms-2"><i class="bi bi-shield-lock"></i> Block</a>' ) ;
136+ const blockPopover = sessionAvailableOnlyActionInfo ( alwaysBlockButton [ 0 ] ) ;
137+ definitiveActions . append ( alwaysBlockButton [ 0 ] ) ;
138+
139+ alwaysBlockButton . on ( 'click' , function ( e ) {
140+ e . preventDefault ( ) ;
141+ addSenderToImagesBlackList ( senderEmail ) . then ( refreshMessageContent . bind ( null , inline ) ) . finally ( ( ) => {
142+ blockPopover . dispose ( ) ;
143+ } )
144+ } ) ;
145+
146+ noticesElement . appendChild ( definitiveActions [ 0 ] ) ;
147+ }
148+
149+ document . querySelector ( '.external_notices' ) . insertAdjacentElement ( 'beforebegin' , noticesElement ) ;
150+ } ;
151+
152+ function handleBlockedStatus ( inline , senderEmail ) {
153+ if ( $ ( '[data-external-resources-blocked="1"]' ) . length ) {
154+ const infoElement = $ ( '<div class="fw-bold">External resources from this sender are blocked.</div>' ) ;
155+ const button = $ ( '<a href="#" class="btn btn-light btn-sm ms-2"><i class="bi bi-unlock"></i> Reset permissions</a>' ) ;
156+
157+ $ ( infoElement ) . append ( button ) ;
158+ $ ( '#externalNoticesAccordion' ) . append ( infoElement ) ;
159+
160+ const popover = sessionAvailableOnlyActionInfo ( button [ 0 ] ) ;
161+
162+ button . on ( 'click' , function ( e ) {
163+ e . preventDefault ( ) ;
164+ removeSenderFromImagesBlackList ( senderEmail ) . then ( refreshMessageContent . bind ( null , inline ) ) . finally ( ( ) => {
165+ popover . dispose ( )
166+ } ) ;
167+ } ) ;
168+
169+ return true ;
170+ }
171+
172+ return false ;
173+ }
174+
175+ function refreshMessageContent ( inline ) {
176+ $ ( '.msg_text_inner' ) . remove ( ) ;
177+ if ( inline ) {
178+ inline_imap_msg ( window . inline_msg_details , window . inline_msg_uid ) ;
179+ } else {
180+ get_message_content ( getParam ( 'part' ) , getMessageUidParam ( ) , getListPathParam ( ) , getParam ( 'list_parent' ) , false , false , false )
181+ }
182+ }
183+
184+ const observeMessageTextMutationAndHandleExternalResources = ( inline ) => {
185+ const message = document . querySelector ( '.msg_text' ) ;
186+ if ( message ) {
187+ new MutationObserver ( function ( mutations ) {
188+ mutations . forEach ( function ( mutation ) {
189+ if ( mutation . addedNodes . length > 0 ) {
190+ mutation . addedNodes . forEach ( function ( node ) {
191+ if ( node . classList . contains ( 'msg_text_inner' ) && ! message . querySelector ( '.external_notices' ) ) {
192+ handleExternalResources ( inline ) ;
193+ }
194+ } ) ;
195+ }
196+ } ) ;
197+ } ) . observe ( message , {
198+ childList : true
199+ } ) ;
200+ }
201+ } ;
0 commit comments