Skip to main content
iVentureTeam

mail_composer_attachment_list

The attachment rows under Odoo's email composer are the mail_composer_attachment_list widget: a stripped-down many2many_binary that lists, opens and deletes, but never uploads.

Siddharth JambukiyaSiddharth JambukiyaOdoo Techno-Functional Consultant
August 24, 2026Updated August 24, 20266 min read
Technical namemail_composer_attachment_list
Field typesmany2many (ir.attachment)
Viewsform (composer and scheduled message dialogs)
Modulemail, shipped with every Odoo database
Used in core3 occurrences in the mail module: composer and scheduled message dialogs
VersionsOdoo 20.0, Odoo 19.0, Odoo 18.0
No-code setupNot applicable: it targets the composer models, not user forms
Alternativesmany2many_binary, binary, html_composer_message

What the composer attachment list does

Attachments in Odoo's full email composer get their own widget. mail_composer_attachment_list takes the wizard's attachment_ids many2many and renders each file as a slim gray row: the filename as a link that opens or downloads the file, and a cross that removes it. That is the whole visible surface.

Technically it subclasses many2many_binary, the standard attachment field, and the interesting part is what the subclass removes. The stock widget renders an upload button and file cards; this one replaces the template with a bare list and no upload affordance at all. Files enter the composer through other doors, the paperclip button of the dialog, pasting into the body, or the records the composer inherited attachments from, and this widget is the single place they are reviewed and removed before sending.

The removal path is where the logic lives: beyond detaching the file from the field, the widget deletes pending composer attachments on the server and broadcasts the removal on the composer's event bus so the message body widget can delete any inline preview of the same file.

What this means for your team

For users, this widget is the last checkpoint before a file leaves the company. The review-then-send habit is worth training: every row under the body is going out to the recipient, including files that arrived automatically because the email was composed from a record that already carried attachments, a quote PDF, a delivery slip, a signed contract. Sales teams that blind-send from templates occasionally ship the wrong revision precisely because nobody looks at these rows.

For administrators, the notable behavior is that removing a pending attachment really deletes it server side rather than leaving an orphan in ir.attachment. Odoo composers historically accumulated orphaned uploads from abandoned drafts; the 18 rework of the composer, this widget included, tightened that lifecycle.

And for anyone extending the composer: because the widget is intentionally upload-less, do not fight it. If your custom wizard needs an upload button in the list itself, use the parent many2many_binary instead and you get the standard uploader back.

Supported options in Odoo 19

Verified against mail_composer_attachment_list.js on the Odoo 19.0 branch. The widget declares no options; the two below are inherited from the many2many_binary descriptor it spreads. Both are honest entries with a caveat: they configure an uploader this template does not render.

OptionTypeWhat it does
accepted_file_extensionsstringInherited from many2many_binary: comma-separated extensions for the file picker. Inert here because this template renders no upload button.
number_of_filesintegerInherited from many2many_binary: hides the upload button once the count is reached. Inert here for the same reason, there is no upload button to hide.

Inherited but inert. accepted_file_extensions and number_of_files are passed through to props exactly as on the parent widget, but this subclass's template contains no upload button, so there is no file picker for them to constrain. They only matter if you re-add an uploader in a further subclass.

Working examples

How mail applies it (composer form)

<field name="attachment_ids" widget="mail_composer_attachment_list" nolabel="1"/>

If you need uploads, use the parent instead

<field name="attachment_ids" widget="many2many_binary"
       options="{'accepted_file_extensions': '.pdf,.png'}"/>

Same field, same storage model, plus the standard Attach button that this composer variant strips away.

What exactly happens when you click the cross

The remove flow, read line by line from the 19.0 source, does three distinct things in order.

Detach. The inherited onFileRemove drops the id from the many2many, which is all the stock widget ever does.

Delete, conditionally. The subclass then looks the attachment up in the mail store and, only when its res_model is mail.compose.message, calls the attachment upload service to unlink it on the server. The guard matters: attachments that belong to the source business record, say the invoice you are emailing, are merely detached from this email and survive on the invoice. Only files uploaded for this draft die with the draft.

Broadcast. Finally it triggers ATTACHMENT_REMOVED on env.fullComposerBus. The body widget listens and strips every element with the matching data-attachment-id, so a pasted image does not linger in the text after its chip is gone.

A version footnote worth knowing: in Odoo 18 that bus trigger was unconditional, so mounting the widget outside the full composer dialog, where the bus does not exist, threw on every removal. 19 added the optional chaining that makes the widget usable standalone, which is also what lets the scheduled message dialog share it safely.

Version compatibility

VersionStatusNotes
Odoo 20.0In developmentNot released. Rebuilt on the Many2XBinary component on the development branch, same behavior contract. See below.
Odoo 19.0VerifiedVerified against the shipped source; composer bus optional, ir.attachment store model.
Odoo 18.0VerifiedFirst release. Crashes on removal when mounted outside the full composer, fixed in 19.
Odoo 17.0Not availableDoes not exist; the composer used a different attachment presentation.
Odoo 16.0Not availableDoes not exist.

Upgrade note for 18 to 19. No XML changes. Two internals changed: the mail store lookup moved to the ir.attachment store model, and the composer bus became optional, fixing the standalone crash. Patches against the 18 class should be rechecked.

What is changing in Odoo 20

Odoo 20 is expected at Odoo Experience in Brussels, 24 to 26 September 2026. Findings below are from the public development branch, which remains unstable until release; we re-verify against the shipped version.

The custom template disappears. On master the widget no longer overrides the list template. Instead it swaps in a subclassed row component on the new Many2XBinary base that the attachment widgets are being rebuilt on, and the service call signature changes to take a list (unlink([attachment])). Visual result is similar; the extension point moves from template inheritance to component substitution, which is exactly where custom composer styling will break.

The conditional server delete and the bus broadcast survive unchanged, so the behavioral contract described above is stable so far. As always with the development branch, we re-check this page once Odoo 20 ships, and our migration team tracks the Many2XBinary rework across all attachment widgets.

Common problems and fixes

SymptomCause and fix
No button to add files in the listBy design: the subclass template removes the uploader. Attach through the composer's paperclip or paste into the body; or use many2many_binary on custom wizards.
Removed an attachment and it vanished from the source record tooOnly attachments whose res_model is mail.compose.message are deleted; record-owned files are merely detached. Check where the file actually lived; record attachments survive removal from the email.
Pasted image still shows in the body after removing its rowThe body sync needs the composer bus; outside the full composer environment nothing listens to ATTACHMENT_REMOVED. Remove the image in the body editor as well, or run inside the standard composer dialog.
accepted_file_extensions seems ignoredThe option configures an upload control this widget does not render. Enforce extensions where the upload happens, or switch to many2many_binary.
Widget errors on file removal in a custom 18 viewOdoo 18 triggered the composer bus unconditionally. Upgrade to 19, or provide a fullComposerBus in the environment, or patch the trigger with optional chaining.

Composer attachment list vs the alternatives

WidgetBest forKey difference
mail_composer_attachment_listReviewing and pruning attachments before an email goes outList-and-remove only, deletes pending uploads server side, syncs with the body
many2many_binaryAttachment fields that should upload as well as listStandard uploader with extension and count options active
binaryA single file on a recordOne value, filename handling via a companion char field
html_composer_messageThe message body the attachments accompanyThe other half of the composer pair, connected over the same bus

The choice is really about which surface owns uploads: this widget assumes the composer's paperclip and body do, the parent widget owns uploads itself, and chatter attachments outside wizards are handled by the chatter, not a field widget.

Frequently asked questions

Why is there no upload button on the composer attachment list?+
The widget's template deliberately renders only filename rows and remove crosses. Uploads belong to the composer's paperclip button and to pasting files into the body; the list is the review surface. The parent many2many_binary widget is the one with a built-in uploader.
Does removing an attachment delete the file?+
Only when the attachment was uploaded for the draft itself, that is, its res_model is mail.compose.message; those are unlinked server side. Files belonging to the business record you are emailing are just detached from the message and remain on the record.
How does the list stay in sync with images pasted in the body?+
Through env.fullComposerBus. When a row is removed the widget fires ATTACHMENT_REMOVED, and the body widget deletes every element with that data-attachment-id. The pair only synchronizes inside the full composer dialog where the bus exists.
Can I use mail_composer_attachment_list on my own model?+
Yes, it works on any many2many pointing at ir.attachment, and since Odoo 19 it no longer requires the composer environment. But you get no upload control, so most custom wizards are better served by many2many_binary.
Which options does it support?+
It declares none. It inherits accepted_file_extensions and number_of_files from many2many_binary, and both are inert in practice because they configure the upload button this template removes.
What changes in Odoo 20?+
The development branch rebuilds the row rendering on the new Many2XBinary component instead of a template override, keeping the delete-and-broadcast behavior. Unstable until the September 2026 release; we re-verify then.

Attachment flows that never send the wrong file

From composer wizards with document checklists to automatic attachment rules per email template, we build the guardrails around Odoo's mailing stack that keep contracts, quotes and revisions straight. One misfiled PDF to a customer costs more than the fix.

Tighten my email attachment flow

How this page was produced

Verified by reading mail_composer_attachment_list.js and its QWeb template on the Odoo 19.0 branch, the parent many2many_binary_field.js descriptor for the inherited options, and diffing 18.0 and the public development branch for the version history, which surfaced the 18 bus crash fix and the master Many2XBinary rebuild. The remove flow, including the res_model guard and the bus broadcast, is described directly from the source. Corrections welcome via our contact page.