nostr_mail 2.6.2
nostr_mail: ^2.6.2 copied to clipboard
A Dart SDK for sending and receiving emails over the Nostr protocol using NIP-59 gift-wrapped messages.
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.