* Run the end-to-end suite on pull requests into milestone and wip branches
A release's work is now collected on a milestone branch (milestone/v0.4.0)
before it reaches main, and the suite only ran on pull requests into main,
so nothing proposed for the milestone was tested. `milestone/**` joins
`main`, and `wip/**` comes along from wip/28, where it already runs the
suite on the phases of #28 — the same block on both, so they merge
without a conflict.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit d009ff72ad)
* linked-issue: read the description of a pull request into a milestone branch (#254)
GitHub links an issue from "Closes #N" only in a pull request into the
default branch. Into milestone/v0.3.8 or a wip/ branch the closing
references stay empty, so the check failed on every such pull request
however its description named the issue (#252 was the first). For a pull
request into another branch, the description is now read for the same
keywords GitHub accepts.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* The release pull request is the suite run behind a tag (#252)
Releasing asked for the E2E workflow to be started by hand on main before
a tag. Since main became protected, the Release vX.Y.Z pull request cannot
merge until that workflow is green on it, and its commit is what is tagged,
so a manual run only ran the same jobs twice. Releasing now says so, and to
update the branch if main moved after its checks ran, since the protection
does not ask for up-to-date branches.
The paragraph in CONTRIBUTING and the header of e2e.yml that said main is
not verified commit by commit describe main before it was protected; both
now describe it as it is.
Closes #251
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Calendar on a phone: a readable month, a toolbar and top bar that fit, a live now line (#256)
The month chip is its title first, on up to two lines, then its time range
and place; a narrower day drops the end and the place for a third line of
title, long words are hyphenated, and an unanswered invitation has a
dashed bar. Below 39rem, where a day is bars, the grid shrinks and the
chosen day is listed in full under it: today at first, any tapped day
after, with a + for that day that the toolbar's + follows too. On a wider
touch screen the first tap opens a day's card and the second its entry
(calendar--month-tap, and a day-open: variant that the hover and the tap
share).
The calendar toolbar fits below 37rem with icons, a short date label and
smaller buttons, and under 22rem today's date in a box for Today. English
writes a same-month week as "Oct 20-26" (range_short_month). The top bar
tightens below 640px and moves Sync now, What's new and Send feedback into
the user menu. Dialogs are the whole screen below 768px, as compose is.
calendar--now moves the now line every minute on the view's clock and
redraws a view that shows the old day or the new one when the date
changes, which also moves the month's circle on today.
And a + on a day that has gone opened the editor on today:
DefaultEventStart::onDay pulled any past day to the next hour. A past day
now keeps its date.
Closes #253
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Close an issue when its pull request is merged into a milestone branch (#260)
GitHub acts on "Closes #N" only for merges into the default branch, so
issues fixed on a milestone branch stayed open until the release reached
main. A workflow on merged pull requests into milestone/** now closes the
issues the description names, with a comment linking the pull request.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Preview the reply, not the quote, and send replies with their quote visible (#258)
The list preview flattened the whole body and skipped short blocks to find
the first long one, so a one-line reply previewed as its attribution line or
the quoted mail. The quoted history (same seam as the reading view's "···")
and a "-- " signature are cut off first now, in HTML and plain text, and the
chrome skip only applies to bulk mail. The JMAP preview uses the same code.
The composer folded the quote with an inline display: none and its clean-up
removed only the toggle, so replies went out with an invisible quote. The
fold is removed in the editor and again on the server, and the reading view
reveals a folded quote in mail already received.
Closes #257
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Move a thread's unread count when JMAP marks a message read or unread (#267)
EmailPatchApplier wrote seenAt for $seen and left MessageThread::$unreadCount alone, so the web list (which draws bold from that counter) disagreed with the phone app. $seen now goes through ThreadStatusUpdater::markRead(), the web UI's own path, and markRead() counts the whole thread rather than the messages passed in.
Closes #266
Co-authored-by: Philippe Delodder <7259755+phdelodder@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Leave a message that is already in the destination folder where it is (#270)
Trashing a conversation with a message already in the bin queued that message as a move from Trash into Trash. webklex's move() then left the connection in EXAMINE and the server refused it ("NO Not permitted in EXAMINE state"), and the row had already lost its UID. ApplyImapFlagsHandler no longer sends a move into the folder a message is in (and gives the row its UID back), and ThreadStatusUpdater's trash(), archive() and restore() leave such messages out of the move.
Closes #269
Co-authored-by: Philippe Delodder <7259755+phdelodder@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Hide a conversation from the lists only when all of it is in the bin (#272)
A thread's labels are the union of its messages', so one deleted message put Trash on the thread and took the whole conversation out of the Inbox, its tab, label lists, All Mail, Unread, the badges and search, along with any reply threaded into it afterwards. The bin rule is now "carries Trash and has no message outside it", written once (binnedThreads()) for every list and badge and spelled out the same way in the search SQL. Option (b) of the issue.
Closes #268
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Let a long message scroll in the expanded compose window (#273)
expandedValueChanged() used closest('div') to reach the editor's wrapper, but closest() starts at the element itself and the editor is a div, so the wrapper never became a flex container and the editor stayed 170px tall, clipped by [contain:paint]. The wrapper is now the flex column and the editor grows (1 0 auto) to fill the window or its content; the plain-text field fills the expanded window too.
Closes #262
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Write each changelog entry as a file of its own, collected at release (#275)
Every pull request added its paragraph under ## Unreleased in CHANGELOG.md at the same place, so the second of any two conflicted with the first once it merged, and each conflict fix re-ran the whole E2E suite. A pull request now adds changelog.d/<issue>-<words>.md; bin/changelog-release.sh, run in the Release vX.Y.Z pull request, collects the fragments (highlights first) and anything still under ## Unreleased into the release's section and deletes them. CHANGELOG.md stays the one file What's new, bin/release-notes.sh and GitHub read.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Say which step failed when notifications can't be turned on (#278)
Every failure in the enable path (registering the service worker, the
browser's push subscription, the server's answer) ended in the same
"Something went wrong" and the exception was dropped, so a report from a
Pixel 8 could not say which. The pane now names the step and the
DOMException name or HTTP status, and logs the exception to the console.
Closes #277
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Contacts: one address book, with Google, Outlook, Nextcloud and CardDAV contacts in it (#282)
A contacts page and pane for plMail's own contacts and the ones on a server:
Google and Outlook on the mail account's own sign-in, Nextcloud through the
Nextcloud connection, any CardDAV server through Settings -> Contacts. Every
field a phone keeps, the photo included, syncs both ways. Duplicates merge
with 90 days to undo; vCard and CSV import and export; contacts suggested
while composing; JMAP Contact/get, /query and /set. Nextcloud calendars come
with the Nextcloud connection too.
Replaces #237 and #109 by @jo2904 and #244 by @DanversKara.
Closes #157
Closes #236
Co-authored-by: jo2904 <jo2904@users.noreply.github.com>
Co-authored-by: DanversKara <DanversKara@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Install APCu and Xdebug with PIE instead of from PECL (#286)
PECL is deprecated in favour of PIE and was down for most of an evening on 2026-10-21, failing every image build. PIE 1.5.2 now installs APCu (and Xdebug in the dev image) from the extensions' own releases; it is mounted for that one step and not left in the image. Nothing changes at runtime.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Keep contact photos where the web container can serve them (#287)
Contact photos were stored in var/contacts, outside the volumes the web and worker containers share, so synced photos answered 404. They now live under APP_STORAGE_DIR/uploads/contacts; a migration clears the lost paths and has synced books read again so the photos come back.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Match a contact's email addresses in any case (#288)
Addresses are stored lower-cased but were matched as the provider spelled them, so a pull removed and re-inserted the same address and broke the unique key (prod sync failure). They are now matched in their stored form.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Open a contact's reference email in the mail pane (#294)
A click on one of a contact's reference emails in the docked contacts
pane was a Turbo visit inside the contacts frame, which blanked the mail
pane. The click is now offered to the mail pane as a mail-pane:open
event; the pane opens the message beside the contact. Without a mail
pane (the full contacts page) it stays an ordinary visit.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Open mail beside the full Contacts page without a reload (#297)
The full Contacts page was a page of its own, so a reference email
clicked there was a visit: the contact was gone, and on a notebook the
mailbox came back on the calendar it remembered.
The full page is now the mailbox with data-contacts-full on the shell:
the mail column put away and the contacts dock given the row, with the
page's own content in the dock's frame. Opening a message from it loads
the message into the existing mail pane, then contacts--pane#reveal
drops the state in a view transition: the mail slides in from the left
and the dock narrows to its width, the same DOM throughout. On a
notebook the calendar gives way; on a wide screen it stays.
The reference links lose data-turbo-frame="_top", which made ui--split
stash a calendar position for a visit that never came.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Preview image and video attachments on click, like PDFs (#295) (#298)
Clicking an image or video attachment chip opens a viewer in the modal
instead of downloading. Previewable chips (PDF included) get a download
half on the right. Images load from the existing attachment route; videos
from a new /mail/attachment/{id}/video route that serves a fixed video
type inline under the sandbox CSP and answers Range requests.
Closes #295
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Erase a message's parts with it, and delete mail one way (#300)
MessageEraser loaded a message's parts to delete their files, then removed
only the message. After the delete Doctrine forgot the message but kept the
parts managed, so the next flush in the same unit of work failed with "a new
entity was found through the relationship MessagePart#message". Every sync
that removed server-deleted mail with an attachment failed that way.
The eraser now removes the parts, removes a thread it empties (announced to
JMAP as destroyed), and logs file-delete failures instead of keeping the row.
MessagePurger hands its local delete to the eraser, so "delete forever" gains
the thread-label re-sync it lacked. ImapAccountSyncer clears an entity manager
a failed folder left open, so one folder's refused flush no longer fails the
rest.
Closes #299
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Rework the contact editor: quick add, paste details and Improve with AI (#305) (#306)
New contacts start in quick add: paste a name, signature, address line or
vCard and the details it finds are listed, each with a one-click type. The
editor shows the name as one field whose parts fill themselves (titles and
degrees included), rows that appear as you type, Paste details for existing
contacts and a Save to picker. Contact help is a new AI feature, off by
default, that rereads pasted text and suggests updates from the signature
of a contact's newest mail. Phones are read with libphonenumber.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Keep spam off the radar and out of search; add a Not spam button (#308)
Cards read out of mail in Spam are left off the Happening Soon panel and
the mailbox strip (read-time filter, so moving the mail out brings them
back), and a spam conversation no longer shows its cards above the
message. The bin keeps its cards. Search and the type-ahead leave Spam
out unless asked with in:spam, as they already did for the bin. A spam
conversation's toolbar gets a Not spam button that moves it to the inbox.
Closes #304
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Add to contacts asks new or existing; contacts button gets the calendar's three positions (#312)
Closes #309
Closes #311
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Show the pictures a review request hid, dropped or never asked for (#274)
* Show the pictures a review request hid, dropped or never asked for
A BAUHAUS review request, sent through Bazaarvoice, showed neither its
logo nor its product photos, with images allowed. The image proxy was
fine; three steps around it were not.
- The logo is linked as http://. The sanitizer keeps http sources and
leaves them to render time, the fetcher only connects over TLS, and
nothing in between upgraded the URL, so every http image became the
placeholder. RemoteContentBlocker now signs the https:// form. The
fetcher's own rules also say in the log which one refused a target;
until now that refusal was the one silent path.
- The first product card sits inside a <form>, and dropElement('form')
took the photo, the name and the star links with it. The form is
unwrapped now; textarea and select still go with their content.
- Each card is class="d" style="display:none", switched on with
.d {display:block !important}. CssToInlineStyles keeps an inline
property whatever the rule says, so the card stayed hidden next to its
Outlook twin. Html5CssInliner now lets a stylesheet !important beat a
plain inline style, and matches selectors against forms as the divs
the reader will see, so `form .a` rules take the sender's fallback
for clients without forms.
app:backfill safe-html-hidden-cards
re-sanitizes stored mail with a form or a stylesheet !important. One
task for both, because neither fix brings the card back alone.
Closes #271
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Mark the German mail page as translated from the current English one
Both pages gained the same two paragraphs in the previous commit, but
the translated-from hash still named the English page before them, so
DocumentationCoverageTest reported the translation as stale.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Credit the image proxy entry and keep it as a changelog fragment
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Paul Lützner <karatektus@gmail.com>
* Swipe a mail row, choose what each swipe does, and long-press for its actions on a phone (#314)
On a phone, below md and by touch only: a row swipes right for read/unread and left for archive, each direction set under Settings → Mail → Swipe actions (read/unread, archive, star, nothing; never trash), and holding a row opens an action sheet with every action its strip offers. Mouse, trackpad and wider windows are unchanged.
Takes over #117, #118 and #119, whose fork was archived. Also gives the Contacts settings' two cards their gap.
Co-authored-by: Flouss <15911342+Flouss@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Admin → Integrations lists the calendar services, so a pinned CalDAV address can be changed later (#279)
CalDAV and Calendar feed get their own group below the file services, with the shared provider row and Configure form; Nextcloud stays with the file services. Calendar services are offered to everyone, so their rows say so and the form has no switch. The server address accepts only http or https, up to 512 characters.
Once people have connected, the CalDAV form says how many connections a changed or cleared pin reaches. A calendar feed offers its address only while one is stored, so it can be cleared.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Offer an address book another connection already mirrors locked, not as new (#319)
Two connections to the same server, a Nextcloud one and a CardDAV one made by hand, reach the same address book under the same remote id. Settings → Contacts now offers such a book locked, saying which connection has it, and refuses a posted tick, as it does for calendars. Otherwise every contact in it showed twice.
Closes #317
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Gmailify: the IMAP folder places a sibling's mail, Gmail deletions detach, and a switch to turn it off (#204)
Closes #203
Closes #205
- A sibling's IMAP copy keeps the placement its folder gives it; Gmail's INBOX/SENT/DRAFT/TRASH/SPAM no longer move it (by @phdelodder).
- Gmail deleting its copy detaches the sibling's IMAP copy instead of erasing it.
- app:backfill gmailify-placement repairs copies the old merge had moved.
- Settings → Aliases: a Gmailify switch per Gmail account. Off detaches carried copies in a background job and declines mail Gmail still collects for another account (Bcc'd mail excepted, as documented).
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Gmail sync: look up only the listed ids, not every stored one (#321)
Part of #223: newGmailIds() asks only about the ids Gmail just listed, and nothing when none were.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Store whether a conversation is in the bin, kept by triggers (#322)
Part of #223: message_thread.binned holds #268's rule, kept by triggers on message_label, thread_label and message; lists, badges and search read the column. Identical results on 41 compared queries, most 5-100x faster on a 67k-thread mailbox.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Account sync: wait out a provider that can't be reached, quietly (#333)
When the worker couldn't connect to the Gmail or Graph API at all, the
account sync went up Messenger's retry ladder: a WARNING with three nested
stack traces per attempt, then CRITICAL and a parked job if the outage
lasted. That job resumes from a cursor and is asked for again every
quarter hour, so nothing was ever lost.
WaitOutConnectionFailures now puts a SyncAccountMessage that failed only
with an HttpClient TransportException back on its own queue, on the
ingest ladder's delays, with one info line per attempt. After five waits
it is dropped with a single warning. Message batches and every other
failure keep the normal path.
Closes #332
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Sven Joder <sj@pmdevelopment.de>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Philippe Delodder <7259755+phdelodder@users.noreply.github.com>
Co-authored-by: jo2904 <jo2904@users.noreply.github.com>
Co-authored-by: DanversKara <DanversKara@users.noreply.github.com>
Co-authored-by: Flouss <15911342+Flouss@users.noreply.github.com>
12 KiB
Contacts
plMail keeps an address book: the people you write to, with their addresses, phone numbers, postal addresses, birthdays, a photo and notes. It is one list, wherever a contact lives. Some are plMail's own and exist only here. Others come from an address book on a server, and changes go both ways:
- a Google or Microsoft mail account brings its contacts with it, on the same sign-in as its mail and calendars — nothing to connect (see Google and Microsoft);
- your Nextcloud, or any other CardDAV server, is connected under Settings → Contacts.
An account connected before plMail synced contacts needs one more sign-in to allow them: Settings → Contacts and Settings → Account health both offer it.
Open it with the address-book icon beside the calendar button. Like the calendar button it is a three-way switch: the address book beside the mail, then full width with your mail behind it, then closed again. Settings → Contacts → Behaviour can turn it into a two-way one (Full width only or Beside mail only), as for the calendar. On a phone, Contacts in the menu opens the full page.
The docked address book is the whole of it: search, open, edit, add and merge contacts there while your mail stays where it was. Drag its edge to make it wider or narrower (or use the arrow keys on the edge; a double-click puts it back). Drag it past its widest and let go, and it goes full width; past its narrowest, and it closes. plMail remembers the width and the position. The arrows in its header make it full width, and the × closes it. Opening a message from a contact at full width brings the mail back beside it.
On a wide monitor (1920 pixels across or more) the address book and the calendar can be docked side by side. On a smaller screen, such as a laptop's, there is room for one of them beside the mail, so opening one puts the other away. Below 1024 pixels the icon opens the full page.
The list
Every contact as a card: photo or initials, name, job and company, and the main email address. Search finds a contact by any of its names, its company or any of its addresses. Tags (you set them on a contact) appear as chips above the list and narrow it to one tag.
Once you have a synced address book, a second row of chips chooses where to look: All books, Not synced (plMail's own contacts), or one address book. A synced contact carries a small cloud, or a lock if its address book is read-only. A clock means an edit made here is waiting to be sent to the server.
A contact
The contact's page shows its details, its notes, related contacts, and Reference emails: every message to or from the contact that has not been deleted, in any folder. Clicking one opens it in the mail beside the contact, which stays as it was; on the full page the mail slides in from the left.
New contact opens quick add: one box to type or paste anything about the person into — a
name, an email signature, a line like Mia Hoffmann <mia@example.net>, an address, or a whole
vCard. It is read as you type, and what it finds appears below as rows: each can be unticked,
retyped, or given another type with one click (a work number that is a home number, a line taken
for the job title that is really the company). A line that could not be placed is asked about
rather than guessed, a greeting above a signature is left out, and an address that is already on
one of your contacts is pointed out. A vCard is read field by field and nothing is guessed. All
fields opens the full editor with everything found already filled in.
The editor shows the name as one field; its parts (prefix, first, middle and last name, suffix) fill themselves as you type and open under Edit parts. Once you edit a part by hand it stays, and the page says when the name and its parts no longer match. Email and phone end in a dashed row — type into it and it becomes a row, typed as home or work from the address, or as a mobile from the number, until you pick a type yourself. The star marks the preferred one. Every other kind of detail shows only when the contact has one; the chips under Add bring the rest. Once something has changed, a bar at the bottom says how much and offers Save and Discard.
Paste details in the editor reads pasted text the same way and compares it with the contact before anything changes: what is already there is greyed out, what is new is ticked, and what differs from what is saved ("Replaces …", or a different type for the same number) is left for you to tick. Apply puts the ticked details into the form and highlights them; nothing is saved until you save.
When your administrator has switched on Contact help (Admin → AI), both pages have Improve
with AI. On quick add it gives the text a second reading — the company the rules could not tell
from a slogan, a fax number written as Fax -99 — and marks what it changed, with Undo AI
changes to go back. In the editor it sends the contact and the signature of the newest mail the
contact sent you, and shows its suggestions, each with its reason, in the same list as pasted
details. You can switch it off for yourself under Settings → Assistant.
A contact holds:
- its name in parts (prefix, first, middle and last name, suffix), a nickname, and how the names are spoken (Phonetic spelling, for names a phone should sort or read out differently);
- company, department, job title and office;
- email addresses, phone numbers and postal addresses, each labelled and one of each marked primary;
- a birthday and other dates (an anniversary, say) — the year can be left out;
- People: the names of a spouse, children, a manager, an assistant and so on;
- websites, chat addresses (SIP, XMPP, Skype, Teams, Matrix and others), and custom fields of your own naming;
- notes, tags and a photo.
A label that came from the server and is not one of plMail's own (a "Godmother" typed on a phone) is shown as it is and kept when you save. If you have a synced address book you can choose where a new contact goes under Save to: one button per address book when you have up to four, a menu when you have more. plMail keeps it in plMail only. Otherwise it goes to that address book's server and from there to every device that syncs it; the line under the buttons says which. A read-only address book is never offered.
Editing works the same for both kinds. For a synced contact, the edit is saved here at once and sent to the server straight after, and from there to your phone. Anything plMail has no field for, such as the properties a phone uses to link a contact to an app, stays on the server's copy untouched. Google and Outlook have no place for some fields; see Things that bite. See CardDAV for how edits and conflicts are handled.
A contact from a read-only address book (a shared book, a company directory) can be read and written to, but not changed or deleted here. Its page says where it comes from.
Photos are JPG, PNG or WebP up to 5 MB, and kept at up to 512 pixels on the longer side. They go both ways: a photo on the server is shown here, and a photo uploaded or removed here is sent to the server. In the editor, the camera on the photo uploads one and Remove photo at the bottom removes it.
Deleting
Delete moves a contact to the bin. Deleting a synced contact also deletes its card on the server, and the contact stays in the bin as one of plMail's own.
Duplicates
Duplicates lists pairs that look like the same person, with how sure the match is and why (the same email address, the same phone number, a similar name). Pick the one to keep and Merge. The other one's addresses, numbers and tags move to the kept contact, and its notes are added below the kept contact's. A merge can be undone for 90 days.
Synced contacts merge like any other. The kept one's card is updated, and the merged one's card is deleted from its server, so an undo brings the merged contact back as one of plMail's own. Contacts from a read-only address book cannot be merged.
Import and export
Import takes a .vcf (vCard, as every phone and address book exports) or a .csv file. You see
a preview first. Imported contacts are plMail's own, and you land on the duplicates page afterwards
to merge anything you already had.
Export downloads every contact as one .vcf file, photos included. A synced contact is exported
as its server's card with your edits in it, so the file keeps what plMail has no field for. The CSV
export is a summary for a spreadsheet — one email, one phone and one website per contact — and reads
back in; use vCard to move contacts whole.
While reading and writing mail
A message's sender, and every address in its details (To, Cc, Reply-To), shows whether it is already in your contacts, with a button to add it if not. The button asks who it is:
- New contact: check the name taken from the mail and save. More details opens the full editor with the name and address filled in.
- Add to an existing contact: search your contacts as you type and pick one. The address is added to that contact, so somebody writing from a second address stays one person. The search starts with the sender's name. Contacts in read-only address books are not offered.
Once saved, the contact opens in the contacts pane beside the mail. If the pane was closed it opens, and if it was open it goes to that contact. More details opens the editor there too. On a phone there is no pane: More details opens the editor as a page, and after a save the button turns into a link to the contact.
When you address a message, your contacts are suggested along with the addresses plMail has learned from your mail. The order is explained under suggestions while writing.
On a phone
The plMail apps read and edit the same contacts over JMAP (Contact/get, Contact/query,
Contact/set). Each contact says which address book it is in and whether it can be changed. An app
cannot change a contact from a read-only book, and its edits to a synced contact go on to the
server like edits made on the web.
Things that bite
The bin has no page yet. A deleted contact is kept, but nothing on the web brings it back. Merge instead of deleting when you are tidying up duplicates: a merge can be undone.
An import makes plMail's own contacts. To put a .vcf file into a synced address book, import
it on the server (in Nextcloud, under Contacts), and the contacts arrive here with the next sync.
A contact cannot move between address books, or from plMail's own into a synced one. Create it in the other place and delete the old one.
Google and Outlook cannot hold everything. Google keeps every field above. Outlook has fixed places: three email addresses, two business and two home numbers and a mobile, one address of each kind, one spouse, manager and assistant and any number of children, one website, three chat addresses and a birthday. It has no other dates, no phonetic middle name and no custom fields. What does not fit is not sent and stays only in plMail until the contact next changes in Outlook (see Microsoft).
Outlook keeps a photo removed here. Microsoft's interface can replace an Outlook contact's photo but not remove it, so a photo removed in plMail comes back the next time the contact changes in Outlook. Remove it in Outlook.
Custom fields and the office are plMail's own on a CardDAV server. vCard has no property for
either, so they are stored on the card under plMail's own names (X-PLMAIL-FIELD,
X-PLMAIL-OFFICE). They survive the round trip, but a phone does not show them.