Skip to content

Attachment Downloads

The messaging module supports downloading image attachments directly from the conversation feed. Downloads produce contact-aware filenames — prefixed with the contact’s name and phone number — so saved files are immediately identifiable.

Two download entry points exist in the UI:

  1. Thumbnail button — a small download icon in the bottom-right corner of each image thumbnail. Clicking it triggers a download without opening the lightbox.
  2. Lightbox toolbar — a download button next to the close button in the full-screen image viewer.

Both append ?download=1 to the existing attachment URL, which tells the backend to respond with a Content-Disposition: attachment header instead of serving the file inline.

GET /api/messages/attachments/:attachment_id?download=1

The download query parameter accepts 1 or true. When omitted, the attachment serves inline (default behavior for lightbox viewing).

Response behavior by storage backend:

Backenddownload=1Default (inline)
R2Redirect to presigned URL with Content-Disposition: attachmentRedirect to presigned URL (inline)
LocalDiskDirect bytes with Content-Disposition: attachment headerDirect bytes with Content-Type only

Authorization — three checks run before serving any attachment:

  1. ViewAttachment collection permission on the message resource
  2. Organization isolation — the attachment’s organization_id must match the session
  3. Contact access — if the attachment belongs to a message linked to a contact, check_contact_access verifies view permission

When download=1 is set and the attachment belongs to a message with a linked contact, the server builds a filename prefix from the contact’s name and phone:

{FirstName}_{PhoneDigits}_{original_filename}

Examples:

ContactPhoneOriginal fileDownload filename
Jane Doe+1 (555) 123-4567photo.jpgJane_15551234567_photo.jpg
Bobimage.pngBob_image.png
photo.jpgphoto.jpg

The prefix uses the contact’s preferred phone number, falling back to their first phone. When no contact or phone exists, the original filename passes through unchanged. If no filename exists at all, the server generates a fallback like attachment.jpg based on the content type.

#[derive(Debug, Default, serde::Deserialize)]
pub struct ServeAttachmentQuery {
/// When `1` or `true`, serves as attachment download.
download: Option<String>,
}

In MessageEntry, each image attachment renders inside a relative container. The download icon (lucide_dioxus::Download) sits absolutely positioned at the bottom-right with a semi-transparent background:

a {
href: "{download_href}", // "/api/messages/attachments/{id}?download=1"
class: "absolute bottom-1.5 right-1.5 p-1.5 rounded-md bg-background/90 ...",
onclick: move |e| e.stop_propagation(), // prevents lightbox open
Download { class: "w-4 h-4" }
}

The stop_propagation() call prevents the click from bubbling to the parent onclick handler that opens the lightbox.

LightboxImage gained a file_name field and a download_url() method:

pub struct LightboxImage {
pub url: String,
pub alt: String,
pub file_name: Option<String>,
}
impl LightboxImage {
pub fn download_url(&self) -> String {
if self.url.contains('?') {
format!("{}&download=1", self.url)
} else {
format!("{}?download=1", self.url)
}
}
}

The LightboxDownloadLink component renders the download button in the lightbox toolbar, using the current image’s download_url().

For R2 downloads, the storage client exposes a dedicated method that bakes Content-Disposition into the presigned URL:

pub async fn presign_get_with_response_content_disposition(
&self,
key: &str,
expires: Duration,
content_disposition: &str,
) -> Result<String, AppError>

This ensures the browser receives the correct filename even when redirected to the R2 presigned URL.

FilePurpose
messaging/api/attachment/serve_attachment_api.rsEndpoint with ?download=1 support and filename generation
messaging/components/message_entry_component.rsThumbnail download button
messaging/components/communication_feed_component.rsPasses file_name to LightboxImage
ui/lightbox_ui.rsLightboxImage, download_url(), and lightbox download button
bases/storage/r2.rspresign_get_with_response_content_disposition
bases/storage/mod.rsStorage trait dispatch for download presigning