Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -660,6 +660,33 @@
- `FakeServer::start_limited` gives a route a use limit; a used-up route is
skipped for the next match (a send that fails once, then succeeds).

## Mail body format (#1901)

- `engine::EngineOptions { body_format }` (`graph::model::BodyFormat`,
default `Text`, re-exported from `sync::engine`) is chosen at spawn:
`spawn` runs the default, `spawn_with_options` takes one. `Text` asks
Graph for plain text (`Prefer: outlook.body-content-type="text"`, lookxy
unchanged); `Html` sends no `Prefer`, so a body comes back native (html,
or text for a plain-text message).
- `bodies.fetched_as` records the format `FetchBody` *asked for*
(`Store::put_fetched_body`), not the content type that came back. NULL is
a body not fetched by `FetchBody`: `put_body` (a `createReply` draft),
`create_local_draft`, `update_draft_fields` (which also makes the body
html, as compose writes html), or a row from before the column.
`Store::get_body_for(id, format)` is the body when it satisfies the format:
a recorded format must equal it; NULL satisfies both when the body is
html or a draft's, else counts as asked for text (the old engine always
did). A draft is `is_draft` or filed in the Drafts folder (synced or the
local sentinel): a complete delta item resets `is_draft`, as `isDraft` is
not in `MESSAGE_SELECT`. So a draft's local body (a plain-text
`createReply`, or html an older `update_draft_fields` left under
`content_type = 'text'`) is never refetched over unsynced edits, a store
filled as text upgrades one message per `FetchBody`, and a native text
answer to `Html` is not refetched. `FetchBody` itself always goes to
Graph: the caller gates on `get_body_for` (`get_body` still shows
whatever is stored).
- Not done: FTS indexes an html body's markup (the triggers copy `content`).

## Format plugins

- A format plugin is a `yppx_*` shared library (`.dll`/`.so`/`.dylib`)
Expand Down
33 changes: 33 additions & 0 deletions mailcore/src/graph/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,39 @@ impl Body {
}
}

/// Which body format the sync engine asks Graph for on a `FetchBody`
/// (#1901). `Text` sends `Prefer: outlook.body-content-type="text"`, so
/// every body comes back as plain text (lookxy, a terminal client). `Html`
/// sends no `Prefer`, so each body comes back in its *native* format: html
/// for an html message, but still text for a plain-text one. That is why
/// the store records the format a body was asked for (`bodies.fetched_as`),
/// not just the content type it came back as.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum BodyFormat {
#[default]
Text,
Html,
}

impl BodyFormat {
/// The value stored in `bodies.fetched_as`.
pub fn as_str(self) -> &'static str {
match self {
BodyFormat::Text => "text",
BodyFormat::Html => "html",
}
}

/// The inverse of [`BodyFormat::as_str`]; `None` for anything else.
pub fn parse(s: &str) -> Option<Self> {
match s {
"text" => Some(BodyFormat::Text),
"html" => Some(BodyFormat::Html),
_ => None,
}
}
}

/// Which Graph attachment kind this is (`@odata.type`). Determines what the
/// UI does on save: `File` downloads its `contentBytes`; `Item` downloads its
/// `/$value` MIME (`.eml`/`.ics`); `Reference` opens `source_url`.
Expand Down
Loading