nostr_mail 3.2.3
nostr_mail: ^3.2.3 copied to clipboard
A Dart SDK for sending and receiving emails over the Nostr protocol using NIP-59 gift-wrapped messages.
3.2.3 #
- Require
ndk: ^0.10.0-dev.6,nostr_event_scheduler: ^0.5.4andsync_engine_shim_for_ndk: ^0.7.1. ndk renamedRelayAuthtoAuthPolicy: code passing one to ndk directly needs the new name.
3.2.2 #
- Creating the schema drops any leftover entity first. A store whose creation
was cut short failed every later open with
index emails_recipient_date already exists; it is now repaired on the next open.
3.2.1 #
- Require
nostr_event_scheduler: ^0.5.3. A scheduled email's status follows the latestkind:7000feedback again, including from a DVM that encrypts it with its own key as the spec now describes, and cancelling a scheduled email tags each DVM its requests named.
3.2.0 #
- Breaking: require
broadcast_queue_shim_for_ndk: ^0.6.0,nostr_event_scheduler: ^0.5.1andsync_engine_shim_for_ndk: ^0.7.0(0.6.x is no longer accepted). AbroadcastQueuepassed tocreatemust now be able to resolve relay lists, so build it withOfflineBroadcast.withNdkor give it arelayListFn. - Labels, private settings and deletions no longer resolve their relays before
queueing: they hand the queue the account's NIP-65 or DM relay list as a
RelaySetand it resolves it on its own, offline included. Adding a label or saving settings no longer waits on a relay list, and the event reaches the relays once the queue has resolved them rather than during the call. - The queue the client owns looks relay lists up on the public indexers plus
defaultDmRelays, not on the indexers alone. - Sending an email keeps resolving its relays itself:
beforePublishstill reports the exact list an event is queued for, and it is still that list a public email names in itspublic-reftag. - Breaking:
getPrivateSettings()is renamedgetLocalPrivateSettings(). It still reads only the decrypted local cache, without a signer. - New: every private settings method takes an optional
pubkey:getLocalPrivateSettings,fetchPrivateSettings,setPrivateSettings,updatePrivateSettingsand the newgetPrivateSettings. It defaults to the logged account and can name any account ndk holds with a signer, so the settings of another account are read and written without switching to it. - Breaking: the
cachedPrivateSettingsgetter is now the methodcachedPrivateSettings({pubkey}), so the synchronous read takes the samepubkey. Add()to existing reads. - New:
getPrivateSettings({pubkey, timeout})reads the private settings local-first, following the local-first reads ADR of ndk (relaystr/ndk#702). Itsstreamemits the local settings with acacheorigin right away, even when there are none, then every newer relay copy with arelaysorigin.(null, relays)means the relays confirmed no settings exist. An undecryptable relay copy, or no relay answering, is a stream error instead ofnull.futurecompletes with the last emission. NdkDataResponse,NdkValueandDataOriginare exported, as a local copy of the ADR types until ndk ships them.- Fix:
removeLabel(andmarkAsUnread,unstar,restoreFromTrash,restoreFromArchive) signs the deletion before removing the label locally. A signer failure now throws and leaves the label in place, instead of removing it on this device only. - Breaking:
flushBroadcasts()andflushBlossomUploads()are removed from the client. Wait onbroadcastQueueandblossomUploadQueuedirectly instead. - Fix: deleting a sent email before the sync brought its gift wrap back
now deletes that wrap on the relays. The deletion only named the rumor id,
which no relay holds, so the wrap stayed there. The sender's own wrap is now
recorded at send time, so
getGiftWrap,getSealandgetRumoralso answer for a sent email right away. - Breaking:
ScheduledEmailStatusgainssending, reported while some of the DVM jobs are published and the others are still queued. Aswitchover the enum must handle it. - New:
ScheduledEmail.statusMessagecarries the DVM's message for the status, typically why a job failed or was rejected. getScheduledEmails()andwatchScheduledEmails()now list the soonest send time first, instead of the most recently created schedule first.
3.1.2 #
- Fix: the listing preview shows the body as the reader sees it. It was
the raw text/plain part, which for an email composed as markdown showed the
syntax and its escapes (
\-\- Sent with [https://x\.org](https://x.org)). The preview now renders the HTML part when there is one, drops quoted replies with theirOn ... wrote:line and everything after the--signature delimiter, and is empty when nothing is left. It is computed once at insert into a newpreviewcolumn, so the schema version moves to 3 and the projection is rebuilt from the NDK cache on upgrade. ScheduledEmail.bodyPreviewfollows the same rules and no longer ends with...: truncation is left to the layout.htmlToText()is exported. It renders HTML as the text/plain alternative a rich-text composer should send: one line per block, links astext <url>, list items as*or1., quotes prefixed with>, and a--line as the--delimiter.Email.bodyuses it when an email has no text part, which also stops the content of<style>elements leaking into the text.
3.1.1 #
getSummaries()filters by sender withsenderPubkeyandfromAddress, the address matched without regard to ASCII case. A bridged email carries the bridge's pubkey, shared by every sender behind it, so list one of them by passing both. The address alone would let any sender claim it.
3.1.0 #
- The sync engine and every account-scoped request name their account as their
NIP-42 identity.
wss://auth.nostr1.com, the first default DM relay, serves nothing anonymously, and a request naming nobody went out under whoever was logged in when the challenge landed. Every account walks its window once more on this upgrade, and a pubkey-only login stops syncing. Requiressync_engine_shim_for_ndk0.6.0 or 0.7.x. - Resolving a recipient's DM relays stays anonymous. The gift wrap goes out under an ephemeral key, so authenticating to find where to send it attached the real sender to the send.
- Fix: the live subscriptions follow the active account. Opened for whoever was logged in and never closed, they left a public email addressed to the previous account landing in the new one's mailbox after a switch.
- Fix: the live subscriptions write what they receive to the NDK cache. An event seen only in real time was projected and then lost until the sync engine walked its period again.
getSummaries()lists a mailbox from the indexed columns alone, a few hundred bytes a row instead of the whole message, and returns aPaginatedResult, now exported. AnEmailparses its MIME on first read of.mimerather than in its constructor.labelsis indexed on(email_id, recipient_pubkey, label), which theemail_statesview needs to resolve a row without rescanning every label of the account. Counting the inbox of a 5000 email store took 360 ms and now takes about 2 ms, as does thegetUnreadCount()thatwatchUnreadCountre-runs on every change.- Schema version 2: the recipients and the sender's display name are indexed, and what a gift wrap yields once opened moves to its own table, so a schema change no longer spends a signer approval. The tables are rebuilt from the NDK cache on first open, and the client now asks for the replay that refills them.
3.0.0 #
- Breaking: the mail store moves from sembast to drift (SQLite).
NostrMailClient.createtakes adatabase: NostrMailDatabase, which the caller opens (NativeDatabaseon native,WasmDatabase.openon web) and closes afterdispose(). The sembastdbis still required: the broadcast queue, the Blossom upload queue, the event scheduler and the sync engine keep theirs. Sembast holds every store in memory, which is what made a large mailbox cost a gigabyte in a browser tab. - Breaking:
kSchemaVersionandmigrateSchemaIfNeededare gone. A schema change bumpsNostrMailDatabase.schemaVersion, and any mismatch drops and rebuilds the tables from the NDK cache when the store opens. - Breaking:
search()runs on an FTS5 index. It matches words and prefixes, case and accent insensitive, instead of arbitrary substrings. - The stores earlier versions kept in the sembast database are dropped on the
first
create(). Nothing is lost: they were a projection of the NDK cache, and the next pass rebuilds them without network. - An email's folder, read and starred state are derived from its labels by a SQL view instead of being copied onto the row. A label landing before its email, or a wrap coming back through the sync, can no longer leave a row out of step with its labels.
- On the web, ship
sqlite3.wasmanddrift_worker.jsinweb/; see the README. - Breaking:
NostrMailClient.createtakes asyncEngine(sync_engine_shim_for_ndk), which replaces NDK's brokenfetchedRanges. It belongs to the caller:create()starts it but never stops nor disposes it. - Breaking:
sync()andresync()are gone.fetchRecent()is the only one left, and it is the pull-to-refresh gesture. Nothing has to be called to stay up to date: the client declares what the active account needs and follows logins and account switches, and the engine revisits on its own. - Breaking:
migrateSchemaIfNeededno longer takes anndk. - Breaking:
getFailedEvents()becomesgetFailedGiftWraps()and returnsFailedGiftWrap, which carries the stage a wrap reached, what stopped it and how many attempts it cost. - Breaking: requires
ndk0.9.0,nostr_event_scheduler0.4.0,broadcast_queue_shim_for_ndk0.5.0,blossom_upload_queue_shim_for_ndk0.7.0 andsync_engine_shim_for_ndk0.4.0. - The NDK cache is now the source of truth for raw events, and the local stores a projection of it replayed on every page that lands. Mail therefore surfaces during a long backfill instead of after it, an event whose processing fails is retried on the next pass unless its recorded failure says another attempt cannot help, and a schema bump rebuilds the stores without going back to the relays.
clearLocalAccountData(pubkey:)andclearAllLocalData()no longer touch NDK state, so clear the NDK cache too to forget an account entirely.- Fix: a sent email no longer loses its labels when its own gift wrap comes back through the sync and rebuilds the row from scratch.
- Fix: an email whose labels landed before it now picks them up. The labels
were saved but had no row to be denormalized onto, so the email stayed in the
inbox, unread and unstarred, while
getLabels()reported it trashed, read or starred. - Fix: deleting an encrypted email now names its gift wrap in the NIP-09
request. Only the rumor id was named, an event no relay has ever held, so the
wrap stayed on the relays forever. NIP-59 has relays honor a deletion signed
by the pubkey in the wrap's
ptag, which is the recipient. The rumor id is still named too: it is what other devices match against their own rows. - Gift wraps are also tombstoned by their own event id now, so a relay re-serving a deleted one costs a lookup per replay instead of unwrapping and unsealing it every time.
- A gift wrap now records why it failed, and that decides what happens next: a
permanent failure (a local key that cannot open it, a malformed body) is
never retried, a transient one (relay, Blossom server, network) is retried on
the next pass, and a remote signer's error is retried
maxSignerAttemptstimes before the wrap is parked. NIP-46 carries no error taxonomy, so a user refusing and a bunker unable to decrypt cannot be told apart, and each attempt can cost the user an approval prompt. - Fix: a remote signer's error no longer marks a gift wrap as processed, which used to drop the mail for good.
- Fix: a gift wrap that arrived while another account was active is
processed when that account comes back, instead of waiting for an explicit
retry. - A gift wrap records its seal and rumor as soon as it yields them, so a failure on the Blossom body resumes at the download instead of asking the signer to decrypt again.
getFailedCount()leaves out permanently failed wraps: anyone can address a malformed wrap to an account, so a count a stranger inflates is not worth showing.getFailedGiftWraps()still lists them.
2.6.2 #
- Every event the SDK enqueues is now attributed to the sending account in the broadcast queue, and every blob upload to the uploading account. Gift wraps are attributed to the sender rather than to their ephemeral event pubkey, so they can be filtered and cleared per account.
- Blob uploads are now signed by the account that queued them instead of by whoever is logged in when a retry fires.
clearLocalAccountData(pubkey:)andclearAllLocalData()now also drop the matching entries in the broadcast and Blossom upload queues, so nothing keeps being retried for an account that logged out. Only queues the client created itself are touched: one passed tocreate()may be shared with other SDKs, so clearing it stays the caller's job.- Entries queued by an earlier version carry no account label. They keep being
retried and are only removed by
clearAllLocalData().
2.6.1 #
- Upgrade
nostr_event_schedulerto 0.3.0 (multi-account, multi-DVM API) and adapt the internalScheduleManager: every scheduler call now acts for the active account explicitly. No public API change. - Bump
broadcast_queue_shim_for_ndkto 0.4.0,blossom_cacheto 0.4.0 andblossom_upload_queue_shim_for_ndkto 0.6.0.
2.6.0 #
- New:
clearLocalAccountData(pubkey:)removes local cache for one account, andclearAllLocalData()removes local cache for every account. Both clear matching NDK fetched ranges so the next sync can rebuild from relays.clearAll()stays as an alias ofclearAllLocalData(). - Fix: Gift wraps are now stored with their own
recipientPubkeyand read back per account, sogetGiftWrap(),getSeal(),getRumor(), retries, the failed-event counters and deletions never touch another account's cache. A wrap that arrives mid account-switch is kept for its own recipient instead of being dropped once its fetched range is marked covered. - Breaking: Bumped local schema version to rebuild gift-wrap records with
recipientPubkey, allowing account-scoped local data cleanup. Existing local caches are wiped once on startup and rebuilt from the relays.
2.5.0 #
- New:
openEmail()opens an email from a Nostr event reference, accepting either a public email event id (kind 1301) or an outer NIP-59 gift wrap id (kind 1059), with optional relay hints for notification/deep-link flows.
2.4.0 #
- New:
sendMime()accepts an optionalbeforePublishcallback, invoked with each fully built outgoing event and its resolved destination relays immediately before it is added to the broadcast queue.
2.3.0 #
- New: Email scheduling through a Scheduler DVM.
scheduleEmail()/scheduleMime()queue an email for future delivery at a given time (one DVM job per outgoing gift wrap or public event);getScheduledEmails()/watchScheduledEmails()list and observe them;cancelScheduledEmail()deletes the package so the DVM never sends;getScheduledMime()reconstructs the full editable MIME to re-open a scheduled email in a composer;resyncScheduledEmails()forces a one-shot resync;startScheduling()/stopScheduling()toggle live DVM feedback and multi-device sync. Configure the DVM viaschedulerDvm/schedulerDvmReadRelaysoncreate(). - New: Exported
ScheduledEmailandScheduledEmailStatus. - New: A scheduled email dates its rumors and MIME
Dateheader at the schedule time, so the recipient sees the send date; the visible gift-wrap envelope stays randomized in the 2 days before it, never revealing that the email was pre-built. - Refactor: Split event building from broadcasting in
EmailSenderbehind aDeliveryabstraction, so immediate and scheduled sends share one build path. - Dependencies: Added
nostr_event_scheduler ^0.2.3. Upgradedndkfrom0.8.4-dev.5to0.8.4-dev.7.
2.2.2 #
- Breaking:
send()/sendMime()take typedRecipientlists (NostrRecipient/SmtpRecipient) perto/cc/bccinstead ofMailAddress; the caller picks each recipient's transport.resolveRecipient()returns aRecipient(not a pubkey), drops itsfromarg, and throws on a NIP-05 network/malformed result instead of misrouting a Nostr recipient to the bridge.Recipienttypes andresolveRecipient()are now exported. - New: Emails to legacy recipients carry the SMTP envelope on the bridge rumor (
mail-from+ onercpt-toperto/cc/bcc), fixing BCC-to-legacy delivery; bridge delivery now applies to public emails too. - Breaking: Raised the minimum Dart SDK version from 3.10.8 to 3.12.0.
- Dependencies: Upgraded
ndkfrom0.8.4-dev.2to0.8.4-dev.5for improved broadcast authentication and live subscription handling. - Refactor: Simplified internal
NostrMailClient,SyncEngine, andBridgeResolverconstructors using Dart 3.12 private named initializing formals. - Tests: Updated
MockRelayto broadcast matching events to live subscriptions and apply NIP-01 replacement rules to replaceable and addressable events. - Tests: Added remote-signer hooks for validating signer-modified event timestamps and content, with safer dynamic tag parsing.
- Tests: Removed the obsolete
mark_unread_persistsintegration test after the mock relay began honoring deletion requests, which meant the test no longer exercised stale label re-delivery.
2.2.1 #
- Fix: NIP-05 recipient resolution now uses
ndk.nip05.resolve()from NDK0.8.4-dev.2, reusing NDK's cache and in-flight request deduplication.
2.2.0 #
- Breaking:
NostrMailClient.delete(...)now acceptsIterable<String>and publishes one batched NIP-09 deletion request with multipleetags, including any NIP-32 label events attached to those emails. - Fix: Local email deletions now create tombstones immediately, so stale relays that ignore NIP-09 cannot re-serve deleted emails into the local cache on the next sync.
- Fix: Deleting an email now removes the associated gift-wrap cache entry by decrypted rumor id instead of assuming the gift-wrap event id equals the email id.
- Fix: Restoring a trashed sent email now returns it to Sent instead of Inbox by deriving the restored mailbox from the sender/owner pubkeys.
2.1.0 #
- Breaking:
getPrivateSettings()is now local-first. It reads the decrypted local settings cache without fetching from relays or requiring a signer. - Breaking: Removed
getCachedPrivateSettings(). UsegetPrivateSettings()for the async local read orcachedPrivateSettingsfor the synchronous in-memory getter. - New:
fetchPrivateSettings()fetches NIP-78 settings from relays, decrypts them, and refreshes the local cache. - Change:
updatePrivateSettings()now reads existing settings through the local-firstgetPrivateSettings()path, then reusessetPrivateSettings()to encrypt and enqueue relay sync. - Fix:
NostrMailClient.create()now primescachedPrivateSettingsfrom local storage when a pubkey is configured. Previously the sync getter stayednulluntil a caller awaited a settings read, which made user-facing settings (signature, bridges, identities) appear reset after sign-out/sign-in: auth-state listeners fired before the new client was constructed, and nothing re-triggered the load once it was.
2.0.1 #
- Fix: Marking an email as unread no longer reverts to read after a refresh. A NIP-09 deletion tombstone store records every deleted label event id and
onLabelAdditionskips any event that has been tombstoned, so a stale label event re-served by a relay that does not honor NIP-09 (or by NDK's in-memory cache, which never acts on deletions) is dropped instead of re-applied. Works the same way for star/unstar and folder restores. - Breaking:
Email.isBridgedis now afinalfield instead of a getter. The previous getter parsed the MIMEFrom:header heuristically; per the nostr-mail-core spec, bridge detection must come from themail-fromtag on the rumor. The parser (parseEmailEvent) andEmailSender._saveSelfCopynow derive the value fromevent.getFirstTag('mail-from') != null, andEmailRecord.toEmail()round-trips it through storage. Callers that constructEmaildirectly must now passisBridged:. - Fix: Inbound nostr-native emails whose sender did not set a MIME
From:header are no longer falsely classified as bridged. The old heuristic returnedtruewhenever the From address was missing or unparseable; the spec-compliant tag check makes the classification deterministic.
2.0.0 #
- Breaking: Attachments no longer live in Sembast. Each attachment is extracted at sync time and stored in
BlossomCachekeyed by its content sha256 (unpinned, LRU-evictable). The original encrypted Blossom blob remains pinned as the source of truth, so any evicted attachment can be regenerated locally without going back to the relays. - Breaking:
Emailno longer exposesrawContent. It now carrieslightMimeText(the RFC 2822 envelope with attachment bodies emptied) andattachmentRefs({ filename, contentType, size, sha256, contentId }). Theemail.mimegetter still returns a parsedMimeMessage, but its attachment parts have empty bodies. - Breaking:
EmailRecordmirrors the same shape change. Existing rows are wiped on upgrade via thekSchemaVersionbump (full resync from relays + Blossom servers). - Breaking:
EmailParser.parseMimeis removed. ConstructEmaildirectly, or useMimeMessage.parseFromTextif all you need is a parsed MIME. - Breaking:
parseEmailEventandSyncEnginenow require a non-nullBlossomCache. - New:
NostrMailClient.getAttachmentBytes(email, ref)- lazy load attachment bytes. Cache hit is instant; cache miss decrypts the source-of-truth blob and re-extracts every attachment, then serves the requested one. - New:
NostrMailClient.getRawMimeText(email)andgetRawMime(email)- reconstruct the original byte-exact RFC 2822 MIME on demand (for.emlexport, reply with full quote, etc.). - Fix: Opening a folder containing emails with large attachments no longer triggers a multi-second MIME parse on every list load (previous behaviour pulled the full base64 attachment off disk for every row, just to display the list).
1.16.0 #
- New: Durable outbound queues -
OfflineBroadcastfor Nostr events andOfflineBlossomUploadfor blob uploads. Both are exposed onNostrMailClient(broadcastQueue,blossomUploadQueue). - New: Local-first
send()/sendMime()/delete()- persisted locally before any network attempt; enqueued for automatic retry until fully delivered. - New:
filters.dart- single source of truth for all 7 Nostr query filters. Eliminates duplication betweenSyncEngineandWatchManager. - Improvement:
SyncEnginenow syncs all filters defined infilters.md(gift wraps, public emails, labels, reposts, settings, metadata/relay lists). Label and deletion filters are now more precise (#L: mail, unified#ktag). - Improvement: Parallelized
fetchRecent()- fetches all 7 filter categories concurrently, then processes events in parallel viaGapSync.processBatch()andFuture.wait. Leverages NDK PR #632 signer concurrency queue to protect remote signers. - Improvement: Downloaded Blossom blobs are now cached locally. Subsequent reparses (e.g. after schema migration) reuse the cached encrypted blob instead of re-downloading.
1.15.0 #
- New: Automatic schema migration on client construction
- Local stores (
emails,labels,gift_wraps,private_settings) and ndk fetched ranges are wiped and rebuilt on every schema version mismatch — the client re-syncs from relays and Blossom on next sync. kSchemaVersionconstant: bump it whenever the shape of any locally stored record changes.migrateSchemaIfNeeded(db:, ndk:)exposed for advanced cases (manual force-resync, tests). Returnstruewhen a migration ran.
- Local stores (
- API change:
NostrMailClient(...)→await NostrMailClient.create(...)- The factory is now async so the migration runs before any repository touches the DB. Update call sites accordingly.
1.14.2 #
- Fix: Removed manual MIME header unfolding - delegate to
enough_mail_pluswhich correctly handles RFC 2822 folding
1.14.1 #
- Improvement: split client.dart in multiple files
1.14.0 #
- New: NIP-18 repost support
repost(Nip01Event emailEvent)— Repost an email to followers using kind 16 generic repost
1.13.0 #
- New: Added
getTrashedEmailsOlderThanmethod to easily query old deleted emails. - Improvement:
saveLabelnow properly stores the original Nostr event'screatedAttimestamp, allowing duration-based queries on labels. - Fix: Resolved state bleeding in tests by ensuring isolated in-memory database filenames for Sembast.
1.12.1 #
- Fix: BCC visibility rules now properly applied for email privacy
removeBccHeaders()— Utility function that removesBccandResent-Bccheaders from MIME messagessendMime()now correctly applies BCC visibility rules:- Sender's copy (keepCopy): sees TO + CC + BCC (all recipients visible)
- TO/CC recipients: sees TO + CC only (BCC hidden)
- BCC recipients: sees TO + CC only (other BCC hidden for privacy)
- BCC recipients are now hidden from TO/CC recipients per email standards
1.12.0 #
- New: Access to technical NIP-59 details (Gift Wrap, Seal, Rumor)
getGiftWrap(emailId)— Retrieve the original kind 1059 eventgetSeal(emailId)— Retrieve the decrypted kind 13 seal eventgetRumor(emailId)— Retrieve the decrypted kind 1301 rumor event
- Improvement: Enhanced Gift Wrap storage
GiftWrapStorenow persists decrypted seals and rumors- Faster retrieval of technical details without re-decryption
- New:
UnwrappedGiftWrapmodel for handling NIP-59 event pairs
1.11.0 #
- New:
identitiesfield inPrivateSettings— a list of RFC 5322MailAddressentries for multi-identity supportidentitiesreplaces the singledefaultAddressconcept with a flexible listdefaultAddressis now a convenience getter returningidentities?.firstupdatePrivateSettings()acceptsidentitiesandclearIdentitiesparameterssend()now uses the first identity as the default "From" address whenfromis not provided
- Breaking:
defaultAddressremoved fromPrivateSettingsconstructor,fromJson,toJson, andcopyWith - Breaking:
updatePrivateSettings()no longer acceptsdefaultAddressorclearDefaultAddressparameters
1.10.0 #
- New: NIP-78 private settings with NIP-44 encryption for cross-device synchronization
PrivateSettingsmodel:signature,defaultAddress,bridges,sourceEventgetPrivateSettings()— fetch and decrypt from relays (write relays, kind 30078)setPrivateSettings()— encrypt and broadcast to relaysupdatePrivateSettings()— update a single field with read-modify-writegetCachedPrivateSettings()— read local decrypted cache (no signer required)cachedPrivateSettings— synchronous getter for in-memory cache (multi-pubkey Map)PrivateSettingsStore— local decrypted JSON cache keyed by pubkey- Settings cleared on
clearAll() - Comprehensive unit and integration tests
1.9.1 #
- Fix: Reduce Blossom threshold from 60KB to 32KB to prevent NIP-44 plaintext limit overflow. NIP-59 double wrapping (Rumor → Seal → Gift Wrap) expands payload size via Base64 + padding, making 60KB unsafe.
1.9.0 #
- Breaking: Refactored
Emailmodel to useMimeMessageinternally for RFC 2822 compliance- Removed direct fields:
from,to,subject,body,date - New API: access parsed data via
email.mime.fromEmail,email.mime.to,email.mime.decodeSubject(),email.mime.decodeTextPlainPart(),email.mime.decodeTextHtmlPart() - Added
htmlBodyandtextBodygetters for direct access datenow uses MIMEDateheader with fallback to Nostr event creation time
- Removed direct fields:
- New:
createdAtfield onEmailto preserve original Nostr event timestamp - New:
rawMimegetter as alias forrawContent - Performance:
EmailReceivedevent now contains fullEmailobject instead of justemailId,from,subject- eliminates redundant database lookups - Performance:
onEmailstream no longer callsgetEmail()for each event - Fix: JSON serialization handles nullable
fromandsubjectfields for malformed emails - Fix: Tests updated to use new MIME-based API throughout
1.8.1 #
- Fix: Switch to
enough_mail_plusto fix critical email header folding issues. This resolves problems where long email addresses inFromheaders were being incorrectly folded, causing SpamAssassin flags and delivery issues. - Improvement: Enhanced RFC-compliance for email rendering.
1.8.0 #
- New: Global email search functionality. Search by subject, body, or sender across all local emails using Sembast regex filters. Search is case-insensitive and handles special characters safely.
1.7.0 #
- New: Support for large emails (> 60KB) via Blossom storage
1.6.1 #
- Fix: Folder labels are now mutually exclusive. When adding a
folder:label (inbox, sent, trash, archive), any existingfolder:label is automatically removed. This prevents emails from appearing in multiple folder views simultaneously when moved between folders.
1.6.0 #
- New: Support for formatted email addresses with display names (e.g.,
"Alice" <alice@uid.ovh>) - New:
resolveRecipient()function extracted toutils/recipient_resolver.dartfor better testability - Improvement: Use
enough_mail'sMailAddress.parse()andencode()for RFC-compliant address formatting - Fix: Domain extraction now works correctly when
fromaddress contains display name
1.5.0 #
- New: Archives helper functions
1.4.5 #
- Fix: html content is encoded in base64
1.4.4 #
- Refactor: improve gift wrap processing and simplify API
1.4.3 #
- Fix: save giftwraps events outside of NDK cache
1.4.2 #
- New: add fetchRecent() for simple parallel sync without fetchedRanges
1.4.0 #
- New:
resync()method to clear fetchedRanges and sync from scratch (useful for recovering late-arriving events) - Improvement: Refactored filter creation into reusable private methods
1.3.1 #
- Bug fix:
recipientPubkeynow correctly extracted from theptag of the email event instead of using the gift wrap recipient - Bug fix: Fallback to HTML body for single-part HTML emails
- Breaking: Emails without a
ptag are now skipped (malformed emails)
1.3.0 #
- New: NIP-32 labels system (
addLabel,removeLabel,moveToTrash,markAsRead,star, etc.) - New: Unified
watch()stream withMailEventsealed class (EmailReceived,EmailDeleted,LabelAdded,LabelRemoved) - New: Convenience stream getters (
onEmail,onTrash,onRead,onStarred,onLabel) - New:
getInboxEmails()andgetSentEmails()with pagination andincludeTrashedoption - New:
htmlBodygetter onEmail(parsed on demand from rawContent) - New:
stopWatching()method to close stream and cleanup resources - Improvement: Local-first labels (save and notify immediately, broadcast in background)
- Improvement: Shared broadcast stream for
watch()(multiple listeners share same subscriptions)
1.2.2 #
- Use the new ndk version
1.2.1 #
- Use the new ndk version
1.2.0 #
- Performance fix:
watchInbox()now useslimit: 0to only receive new real-time events, avoiding re-processing of historical gift wraps at startup - New:
sync()now accepts optionallimit,since, anduntilparameters for incremental sync
1.1.1 #
- Security fix: Added
recipientPubkeyfield to Email model to properly filter emails by recipient - Performance fix: Mark all gift wraps as processed after decryption to avoid re-decrypting DMs and other non-email content on each sync
1.1.0 #
- RFC 2822 compatibility: addresses without domain now get
@nostrsuffix - Standardized on npub format for all Nostr addresses (hex pubkeys auto-converted)
- Fixed: "To" field was empty when sending to npub addresses
1.0.0 #
- Initial version.