Skip to content

Commit 812cf51

Browse files
jbpenrathmosa-riel
andauthored
✨(all) allow to display external images through proxy (#469)
Allow to users to display external images from their email through a secure proxy endpoint to ensure security and privacy and respect iframe csp policy. Co-authored-by: =?UTF-8?q?Ri=C3=ABl=20Notermans?= <riel@mosa.cloud>
1 parent a67fd36 commit 812cf51

24 files changed

Lines changed: 1822 additions & 84 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ and this project adheres to
1515
- Allow to search for spam messages
1616
- Add `is_trashed` flag to thread model
1717
- Add to select multiple threads in thread panel
18+
- Add image proxy endpoint to display external images in messages
1819

1920
### Changed
2021

docs/env.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,17 @@ _Those settings are deprecated and will be removed in the future._
286286
| `FEATURE_AI_SUMMARY` | `False` | Default enabled mode for summary AI features | Required |
287287
| `FEATURE_AI_AUTOLABELS` | `False` | Default enabled mode for label AI features | Required |
288288

289+
### Image Proxy
290+
291+
**Note**: By default `IMAGE_PROXY_MAX_SIZE` is set to 5MB. We do not encourage to increase this value as
292+
it can lead to memory exhaustion, increase at your own risk.
293+
294+
| Variable | Default | Description | Required |
295+
|----------|---------|-------------|----------|
296+
| `IMAGE_PROXY_ENABLED` | `False` | Whether external images should be proxied | Optional |
297+
| `IMAGE_PROXY_MAX_SIZE` | `5242880` (5MB) | Maximum size in bytes for external images | Optional |
298+
| `IMAGE_PROXY_CACHE_TTL` | `2592000` (30 days) | Cache TTL in seconds for external images | Optional |
299+
289300
### Third-party Services
290301

291302
#### Drive

src/backend/core/api/openapi.json

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,11 @@
227227
"type": "integer",
228228
"description": "Maximum number of recipients per message (to + cc + bcc)",
229229
"readOnly": true
230+
},
231+
"IMAGE_PROXY_ENABLED": {
232+
"type": "boolean",
233+
"description": "Whether external images should be proxied",
234+
"readOnly": true
230235
}
231236
},
232237
"required": [
@@ -241,7 +246,8 @@
241246
"MAX_OUTGOING_ATTACHMENT_SIZE",
242247
"MAX_OUTGOING_BODY_SIZE",
243248
"MAX_INCOMING_EMAIL_SIZE",
244-
"MAX_RECIPIENTS_PER_MESSAGE"
249+
"MAX_RECIPIENTS_PER_MESSAGE",
250+
"IMAGE_PROXY_ENABLED"
245251
]
246252
}
247253
}
@@ -2418,6 +2424,57 @@
24182424
}
24192425
}
24202426
},
2427+
"/api/v1.0/mailboxes/{mailbox_id}/image-proxy/": {
2428+
"get": {
2429+
"operationId": "mailboxes_image_proxy_list",
2430+
"description": "Proxy an external image through the server.\n\n This endpoint fetches images from external sources and serves them\n through the application to protect user privacy. Requires the\n IMAGE_PROXY_ENABLED environment variable to be set to true.\n ",
2431+
"parameters": [
2432+
{
2433+
"in": "path",
2434+
"name": "mailbox_id",
2435+
"schema": {
2436+
"type": "string"
2437+
},
2438+
"description": "ID of the mailbox",
2439+
"required": true
2440+
},
2441+
{
2442+
"in": "query",
2443+
"name": "url",
2444+
"schema": {
2445+
"type": "string"
2446+
},
2447+
"description": "The external image URL to proxy",
2448+
"required": true
2449+
}
2450+
],
2451+
"tags": [
2452+
"mailboxes"
2453+
],
2454+
"security": [
2455+
{
2456+
"cookieAuth": []
2457+
}
2458+
],
2459+
"responses": {
2460+
"200": {
2461+
"description": "Image content"
2462+
},
2463+
"400": {
2464+
"description": "Invalid request"
2465+
},
2466+
"403": {
2467+
"description": "Forbidden"
2468+
},
2469+
"413": {
2470+
"description": "Image too large"
2471+
},
2472+
"502": {
2473+
"description": "Failed to fetch external image"
2474+
}
2475+
}
2476+
}
2477+
},
24212478
"/api/v1.0/mailboxes/{mailbox_id}/message-templates/": {
24222479
"get": {
24232480
"operationId": "mailboxes_message_templates_list",

src/backend/core/api/viewsets/blob.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,8 @@ def download(self, request, pk=None):
185185
f'attachment; filename="{attachment["name"]}"'
186186
)
187187
response["Content-Length"] = attachment["size"]
188+
# Enable browser caching for 30 days (inline images benefit from this)
189+
response["Cache-Control"] = "private, max-age=2592000"
188190

189191
else:
190192
# Get the blob
@@ -211,6 +213,8 @@ def download(self, request, pk=None):
211213
# Add appropriate headers for download
212214
response["Content-Disposition"] = f'attachment; filename="{filename}"'
213215
response["Content-Length"] = blob.size
216+
# Enable browser caching for 30 days (inline images benefit from this)
217+
response["Cache-Control"] = "private, max-age=2592000"
214218

215219
return response
216220

src/backend/core/api/viewsets/config.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,11 @@ class ConfigView(drf.views.APIView):
9696
),
9797
"readOnly": True,
9898
},
99+
"IMAGE_PROXY_ENABLED": {
100+
"type": "boolean",
101+
"description": "Whether external images should be proxied",
102+
"readOnly": True,
103+
},
99104
},
100105
"required": [
101106
"ENVIRONMENT",
@@ -110,6 +115,7 @@ class ConfigView(drf.views.APIView):
110115
"MAX_OUTGOING_BODY_SIZE",
111116
"MAX_INCOMING_EMAIL_SIZE",
112117
"MAX_RECIPIENTS_PER_MESSAGE",
118+
"IMAGE_PROXY_ENABLED",
113119
],
114120
},
115121
)
@@ -127,6 +133,7 @@ def get(self, request):
127133
"LANGUAGE_CODE",
128134
"SCHEMA_CUSTOM_ATTRIBUTES_USER",
129135
"SCHEMA_CUSTOM_ATTRIBUTES_MAILDOMAIN",
136+
"IMAGE_PROXY_ENABLED",
130137
]
131138
dict_settings = {}
132139
for setting in array_settings:

0 commit comments

Comments
 (0)