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
15 changes: 14 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ released versions carry their date on the heading.
email address on Text Message is still an email address, because iMessage
reaches one.

#### Search
#### Browsing and search

- 2026-10-05: **A search no longer finds an Unsent message by the text it
hides.** An Unsent message reads "Unsent" and nothing else, but when an
Expand All @@ -118,6 +118,16 @@ released versions carry their date on the heading.
`from:`, the conversation and the date still find an Unsent message, and a
message Deleted in the source app is still found by its text.

- 2026-10-05: **A message keeps the milliseconds of its time.** Message
Crate kept a message's time to the whole second and dropped the
milliseconds that WhatsApp, Apple Messages and SMS Backup & Restore
record. Two messages sent within one second could then show
in the wrong order. The time is now kept to the millisecond, a
conversation lists its messages in the order they were sent, and an
export written from Message Crate keeps the milliseconds too. A backup
that records whole seconds, such as iMazing or OpenExtract, lists the
messages of one second in the order the backup gives them, as before.

### Upgrading

- The database format changed. **An existing Message Crate is rebuilt empty
Expand All @@ -135,6 +145,9 @@ released versions carry their date on the heading.
in place of `num_replies`.
- A Saved Search that uses `deleted:yes` finds fewer messages than before:
it leaves unsent messages out. Add `or unsent:yes` to it to find both.
- If you have a program that reads messages from the HTTP API, a message's
`timestamp` and an earlier version's `edited_at` now carry three digits of
milliseconds, such as `2015-03-12T18:04:22.250Z`, where they had none.

## [0.10.1] - 2026-10-05

Expand Down
13 changes: 8 additions & 5 deletions crates/libs/api-types/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -372,8 +372,11 @@ api_shape! {
/// Export GUID for replies and grouping. Every message has one,
/// because the import refuses a message without one.
pub guid: String,
/// The instant the message was sent: RFC 3339 in UTC with a `Z`
/// suffix. A caller shows it in the account's time zone
/// The instant the message was sent, to the millisecond: RFC 3339
/// in UTC with three fractional digits and a `Z` suffix
/// (`2015-03-12T18:04:22.250Z`; `.000` when the source records
/// whole seconds). Messages are listed in the order of this time. A
/// caller shows it in the account's time zone
/// (`Account.time_zone`); the database stores nothing
/// about where the phone was.
pub timestamp: String,
Expand Down Expand Up @@ -450,8 +453,8 @@ api_shape! {
pub part_index: i64,
/// The part's text in this version.
pub text: String,
/// When this version was written: RFC 3339 in UTC with a `Z` suffix,
/// as `Message.timestamp`. The original's is when it was sent, a
/// When this version was written, to the millisecond in the form
/// `Message.timestamp` takes. The original's is when it was sent, a
/// later version's is when the edit that wrote it was made. `None`
/// when the source does not record it.
pub edited_at: Option<String>,
Expand Down Expand Up @@ -603,7 +606,7 @@ mod tests {
source: "imessage".into(),
service: None,
guid: "g1".into(),
timestamp: "2024-01-01T00:00:00Z".into(),
timestamp: "2024-01-01T00:00:00.000Z".into(),
sort_order: 0,
is_from_me: false,
sender: None,
Expand Down
20 changes: 10 additions & 10 deletions crates/server/server/src/accounts_api/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2069,13 +2069,13 @@ async fn the_identities_route_counts_the_direct_and_group_messages_held_at_each_
let two = [
SeedMessage {
source: "imessage",
timestamp: "2020-01-01T00:00:00Z",
timestamp: "2020-01-01T00:00:00.000Z",
is_from_me: true,
body: "a",
},
SeedMessage {
source: "imessage",
timestamp: "2020-01-02T00:00:00Z",
timestamp: "2020-01-02T00:00:00.000Z",
is_from_me: false,
body: "b",
},
Expand All @@ -2095,19 +2095,19 @@ async fn the_identities_route_counts_the_direct_and_group_messages_held_at_each_
let three = [
SeedMessage {
source: "imessage",
timestamp: "2020-02-01T00:00:00Z",
timestamp: "2020-02-01T00:00:00.000Z",
is_from_me: true,
body: "c",
},
SeedMessage {
source: "imessage",
timestamp: "2020-02-02T00:00:00Z",
timestamp: "2020-02-02T00:00:00.000Z",
is_from_me: false,
body: "d",
},
SeedMessage {
source: "imessage",
timestamp: "2020-02-03T00:00:00Z",
timestamp: "2020-02-03T00:00:00.000Z",
is_from_me: false,
body: "e",
},
Expand Down Expand Up @@ -2192,17 +2192,17 @@ async fn the_identities_route_counts_the_direct_and_group_messages_held_at_each_
{
"address": "+15555550100",
"service": "phone",
"start_date": "2020-01-01T00:00:00Z",
"end_date": "2020-02-02T00:00:00Z",
"start_date": "2020-01-01T00:00:00.000Z",
"end_date": "2020-02-02T00:00:00.000Z",
"conversations": 2,
"direct_messages": 2,
"group_messages": 2
},
{
"address": "alice@example.com",
"service": "email",
"start_date": "2020-02-03T00:00:00Z",
"end_date": "2020-02-03T00:00:00Z",
"start_date": "2020-02-03T00:00:00.000Z",
"end_date": "2020-02-03T00:00:00.000Z",
"conversations": 1,
"direct_messages": 0,
"group_messages": 1
Expand Down Expand Up @@ -2243,7 +2243,7 @@ async fn the_storage_route_sums_attachment_bytes_and_lists_the_largest_first() {
source_file: "seed.jsonl",
messages: &[SeedMessage {
source: "imessage",
timestamp: "2020-01-01T00:00:00Z",
timestamp: "2020-01-01T00:00:00.000Z",
is_from_me: true,
body: "photos",
}],
Expand Down
2 changes: 1 addition & 1 deletion crates/server/server/src/assets_api/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1489,7 +1489,7 @@ pub(crate) async fn seed_attachment_with_preview(
source_file: "seed.jsonl",
messages: &[crate::test_support::SeedMessage {
source: "imessage",
timestamp: "2020-01-01T00:00:00Z",
timestamp: "2020-01-01T00:00:00.000Z",
is_from_me: true,
body: "two photos",
}],
Expand Down
30 changes: 15 additions & 15 deletions crates/server/server/src/contacts_api/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -562,8 +562,8 @@ async fn get_contact_detail_counts_direct_group_and_messages() {
.await
.unwrap();
for (body, ts) in [
("hi", "2024-06-01T12:00:00Z"),
("there", "2024-06-01T13:00:00Z"),
("hi", "2024-06-01T12:00:00.000Z"),
("there", "2024-06-01T13:00:00.000Z"),
] {
MessageRow {
timestamp: ts,
Expand All @@ -576,7 +576,7 @@ async fn get_contact_detail_counts_direct_group_and_messages() {
}
// A reply of yours: not a message Sam sent, so not in `total_messages`.
MessageRow {
timestamp: "2024-06-01T14:00:00Z",
timestamp: "2024-06-01T14:00:00.000Z",
is_from_me: true,
body: Some("back at you"),
..MessageRow::new(account, 1)
Expand Down Expand Up @@ -612,7 +612,7 @@ async fn get_contact_detail_counts_direct_group_and_messages() {
.await
.unwrap();
MessageRow {
timestamp: "2024-07-01T12:00:00Z",
timestamp: "2024-07-01T12:00:00.000Z",
sender_handle_id: Some(peer),
body: Some("group hi"),
..MessageRow::new(account, 2)
Expand All @@ -636,7 +636,7 @@ async fn get_contact_detail_counts_direct_group_and_messages() {
.await
.unwrap();
MessageRow {
timestamp: "2024-08-01T12:00:00Z",
timestamp: "2024-08-01T12:00:00.000Z",
body: Some("nope"),
..MessageRow::new(account, 9)
}
Expand Down Expand Up @@ -665,11 +665,11 @@ async fn get_contact_detail_counts_direct_group_and_messages() {
assert_eq!(detail.identities[0].group_messages, 1);
assert_eq!(
detail.identities[0].start_date.as_deref(),
Some("2024-06-01T12:00:00Z")
Some("2024-06-01T12:00:00.000Z")
);
assert_eq!(
detail.identities[0].end_date.as_deref(),
Some("2024-07-01T12:00:00Z")
Some("2024-07-01T12:00:00.000Z")
);
}

Expand Down Expand Up @@ -719,8 +719,8 @@ async fn get_contact_summaries_counts_two_contacts_in_one_query() {
.await
.unwrap();
for (body, ts) in [
("hi", "2024-06-01T12:00:00Z"),
("there", "2024-06-01T13:00:00Z"),
("hi", "2024-06-01T12:00:00.000Z"),
("there", "2024-06-01T13:00:00.000Z"),
] {
MessageRow {
timestamp: ts,
Expand Down Expand Up @@ -758,7 +758,7 @@ async fn get_contact_summaries_counts_two_contacts_in_one_query() {
.await
.unwrap();
MessageRow {
timestamp: "2024-07-01T12:00:00Z",
timestamp: "2024-07-01T12:00:00.000Z",
sender_handle_id: Some(sam_handle),
body: Some("group hi"),
..MessageRow::new(account, 2)
Expand Down Expand Up @@ -806,7 +806,7 @@ async fn get_contact_summaries_counts_two_contacts_in_one_query() {
.await
.unwrap();
MessageRow {
timestamp: "2024-05-01T09:00:00Z",
timestamp: "2024-05-01T09:00:00.000Z",
sender_handle_id: Some(pat_handle),
body: Some("hey"),
..MessageRow::new(account, 3)
Expand All @@ -827,11 +827,11 @@ async fn get_contact_summaries_counts_two_contacts_in_one_query() {
assert_eq!(summaries[0].group_message_count, 1);
assert_eq!(
summaries[0].start_date.as_deref(),
Some("2024-06-01T12:00:00Z")
Some("2024-06-01T12:00:00.000Z")
);
assert_eq!(
summaries[0].end_date.as_deref(),
Some("2024-07-01T12:00:00Z")
Some("2024-07-01T12:00:00.000Z")
);

assert_eq!(summaries[1].id, pat_id);
Expand All @@ -842,11 +842,11 @@ async fn get_contact_summaries_counts_two_contacts_in_one_query() {
assert_eq!(summaries[1].group_message_count, 0);
assert_eq!(
summaries[1].start_date.as_deref(),
Some("2024-05-01T09:00:00Z")
Some("2024-05-01T09:00:00.000Z")
);
assert_eq!(
summaries[1].end_date.as_deref(),
Some("2024-05-01T09:00:00Z")
Some("2024-05-01T09:00:00.000Z")
);
}

Expand Down
Loading
Loading