Skip to content

sync: send AS 16.0 draft messages (email2:Send) with an explicit sender - #31

Open
Squareys wants to merge 2 commits into
grommunio:masterfrom
Squareys:fix/as16-draft-send
Open

sync: send AS 16.0 draft messages (email2:Send) with an explicit sender#31
Squareys wants to merge 2 commits into
grommunio:masterfrom
Squareys:fix/as16-draft-send

Conversation

@Squareys

Copy link
Copy Markdown

Problem

AS 16.0 clients (e.g. Samsung Email) send a message saved in Drafts via a Cmd=Sync request carrying an empty <Send/> tag ([MS-ASEMAIL] §2.2.2.69). The Sync parser didn't expect <Send/> after </ApplicationData>, so it aborted with an uncaught WBXMLExceptionHTTP 500. The client wraps the 500 as a network error, so the message is stuck in the Outbox, silently, and retries fail identically. One-step Cmd=SendMail is unaffected — only the "was in Drafts" path breaks.

Fix

  • lib/request/sync.php — consume the empty email2:Send tag (sibling of ApplicationData inside Add/Change) and flag the message for sending. This alone fixes the 500.
  • lib/grommunio/importer.php — actually send the flagged draft: set the sender (PR_SENDER_* + PR_SENT_REPRESENTING_*) explicitly to the authenticated mailbox owner via mapi_createoneoff, then mapi_message_submitmessage. The draft is removed only after a successful submit; on failure a StatusException is thrown and the draft is kept.

These drafts typically carry an empty From. Setting the sender explicitly — rather than routing through SendMail(), which strips PR_SENT_REPRESENTING_* to rely on transport auto-fill — guarantees a valid From regardless of the deployed gromox version or a From-strict MX. Otherwise a sender-less message can be silently dropped downstream after the draft is already deleted (invisible mail loss).

Testing

  • End-to-end on a local gromox rig: a Samsung-style empty-From draft-send delivers with From: = the mailbox owner, and the draft is removed only after a successful submit.
  • A stubbed-MAPI unit test additionally asserts: explicit owner sender set from an empty From, submit called, draft deleted only after success, and draft kept on submit failure. Not included here since the repo has no test suite — happy to add it under a tests/ dir if you'd like.

Reference

[MS-ASEMAIL] §2.2.2.69 — Send element: empty tag; presence = send, absence = save as draft; supported in protocol 16.0 / 16.1.

AS 16.0 clients (e.g. Samsung Email) send a Drafts message via Cmd=Sync
with an empty <Send/> tag ([MS-ASEMAIL] 2.2.2.69). The Sync parser did
not expect <Send/> after </ApplicationData> and aborted with an uncaught
WBXMLException -> HTTP 500; the client reports it as a network error and
the message stays silently stuck in the Outbox.

lib/request/sync.php: consume the empty email2:Send tag and flag the
message for sending.

lib/grommunio/importer.php: send the flagged draft. These drafts often
carry an empty From, so set PR_SENDER_* / PR_SENT_REPRESENTING_* to the
authenticated mailbox owner explicitly (via mapi_createoneoff) before
mapi_message_submitmessage, rather than relying on transport auto-fill
(SendMail() strips PR_SENT_REPRESENTING_*) which is absent on some gromox
versions and would let a From-less message be dropped downstream after
the draft was already deleted. The draft is removed only after a
successful submit; on failure a StatusException is thrown and the draft
is kept.
@gromandreas

Copy link
Copy Markdown
Member

Hi @Squareys,

thank you for the contribution. I have a couple of questions/notes though. I'll address them as comments in the relevant parts of code.

Comment thread lib/grommunio/importer.php Outdated

$response->serverid = $this->prefix . bin2hex((string) $sourcekeyprops[PR_SOURCE_KEY]);

// AS 16.0+: send the message if the client requested it (draft send).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's very similar to the commit message. In my opinion this comment may be shorter.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Trimmed it a bit, let me know if that's better.

// has already been removed => invisible mail loss. Instead set the
// sender explicitly to the authenticated mailbox owner, so a valid From
// is guaranteed regardless of the deployed gromox version, then submit.
if ($message instanceof SyncMail && !empty($message->send)) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do it here and not in the MAPIProvider::setEmail()? That would reduce the number of mapi_*() calls.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No particular reason, just new to the codebase! Thanks for the suggestion, feels much cleaner.

Review feedback on PR grommunio#31: the explicit-sender stamping added to
ImportChangesICS->ImportMessageChange() duplicated MAPI work that
MAPIProvider->setEmail() already does. Fold the PR_SENDER_* /
PR_SENT_REPRESENTING_* assignment into the mapi_setprops() that setEmail
already issues, so the send path drops one mapi_setprops() and one
mapi_savechanges() (the importer's existing mapi_savechanges() persists
the sender before submit).

The importer now only submits the flagged draft and removes it after a
successful submit; on failure a StatusException is thrown and the draft
is kept. Behaviour is unchanged: an AS 16.0 draft send still goes out
with the authenticated mailbox owner as From regardless of gromox
version.
@Squareys

Copy link
Copy Markdown
Author

Thanks for the review @gromandreas! I addressed your comments with e31e8e6

Feel free to add further comments, if you notice anything else.

@Squareys

Squareys commented Aug 4, 2026

Copy link
Copy Markdown
Author

@gromandreas Happy to do another iteration, if you find anything you'd like changed!

}
// a valid one-off ENTRYID is required or the spooler rejects the empty
// sent-representing the client left behind
$owneroneoff = mapi_createoneoff($ownername, "SMTP", $owneraddr);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In SendMail() in grommunio.php (

// Delete the PR_SENT_REPRESENTING_* properties because some android devices
) we simply unset a bunch of the following properties in order to make the spooler happy. That might work in this case as well (it would make sense to rework SendMail() as well even if it works fine now).
I'll to do some more testing this week if I get to it. Some of the colleagues are on vacation and I'm pretty swamped with work, sorry if it takes somewhat longer.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants