Compare commits

..

257 Commits

Author SHA1 Message Date
Ludovic LANGE 66c6b62aa6
Cargo.lock: Update lexical-core version
Fixes compilation on macos 10.15.3, rustc 1.53.0
2021-07-05 23:41:55 +03:00
Manos Pitsidianakis eea9ac2b58
README.md: update with new IRC channel location 2021-06-13 11:27:33 +03:00
Manos Pitsidianakis d16866e0f0
notifications: run update_xbiff even if notifications disabled 2021-01-15 16:41:40 +02:00
Manos Pitsidianakis bcca9abe66
docs: Use example.com in documentation
Closes #96
2021-01-15 16:41:40 +02:00
Manos Pitsidianakis 24b4c117e7
melib: don't use both {set,push}_references()
set_references() already calls push_references()
2021-01-15 16:41:40 +02:00
Manos Pitsidianakis b0fba401e6
melib/mbox: consistent line endings in MboxFormat::append 2021-01-11 19:11:08 +02:00
Manos Pitsidianakis 48d4343082
utilities/ProgressSpinner: add interval field and new spinners 2021-01-11 19:11:08 +02:00
Manos Pitsidianakis 2dfeb29b75
jobs/Timer: add set_interval() 2021-01-11 19:11:08 +02:00
Manos Pitsidianakis 63d2fb93f4
melib/nntp: fix not connecting with TLS 2021-01-11 19:11:08 +02:00
Manos Pitsidianakis cf9457882a
melib/mbox: add MboxMetadata type and write support 2021-01-11 19:11:08 +02:00
Manos Pitsidianakis 3fa9e355c2
melib/email: add Flag is_*() methods 2021-01-11 18:46:22 +02:00
Manos Pitsidianakis 3dae84182c
melib/mbox: add module-level doc 2021-01-11 18:46:11 +02:00
Manos Pitsidianakis a4ae4da8b1
Add export-mbox command 2021-01-10 01:45:03 +02:00
Manos Pitsidianakis 4050f6893f
melib/mbox: add MboxFormat::append() method
Add support for writing mbox files
2021-01-10 01:40:54 +02:00
Manos Pitsidianakis dcccd303ac
melib/mbox: rename MboxReader to MboxFormat 2021-01-10 01:40:54 +02:00
Manos Pitsidianakis 22a64e2d76
melib: Remove unnecessary "pub use" std exports 2021-01-10 01:40:27 +02:00
Manos Pitsidianakis 781a1d0e1b
melib/backends: add collection() method to MailBackend
Keep track of the Collection state in the backend side
2021-01-10 01:31:27 +02:00
Manos Pitsidianakis eb8d29813c
utilities/Tabbed: send VisibilityChange event on changing tab 2021-01-08 18:37:51 +02:00
Manos Pitsidianakis 08af46f5ef
melib/datetime: fix test compile failure 2021-01-08 18:37:51 +02:00
Manos Pitsidianakis 2f47f1eebd
melib/jmap: fix mailbox children relationships being ignored 2021-01-08 15:23:25 +02:00
Manos Pitsidianakis 622ded8021
compose: add attribution line for replies 2021-01-08 15:01:38 +02:00
Manos Pitsidianakis 6d63429ad3
Add scrolling context to StatusBar
- Whenever a scrolling context is entered/exited, send a ScrollUpdate event.
- StatusBar maintains a stack of scrolling contexts and displays the
last one, if it exists. Each context is associated with a ComponentId.
- To handle dangling contexts after their Components aren't visible
anymore, send a VisibilityChange event in situations where that scenario
is possible.
2021-01-08 15:01:38 +02:00
Manos Pitsidianakis 5eb4342af8
Update dependencies, update indexmap to ^1.6 2021-01-08 15:01:38 +02:00
Manos Pitsidianakis eca10a5660
melib/backends: add mailbox management events to RefreshEventKind
Add mailbox management events from RFC 5423 Internet Message Store
Events

https://tools.ietf.org/html/rfc5423#page-8
2021-01-08 15:01:38 +02:00
Manos Pitsidianakis a697dfabbd
melib/jmap: use receivedAt as alternative to Date in Envelope gen 2021-01-08 15:01:38 +02:00
Manos Pitsidianakis 23997bdec0
melib/jmap: add UTCDate queries in EmailFilterCondition
Not necessarily working, added as stubs for future work

Closes #62
2021-01-08 15:01:37 +02:00
Manos Pitsidianakis 2e6a1e1ef8
melib/datetime: rename tests for consistency 2021-01-08 15:01:37 +02:00
Manos Pitsidianakis fe200a3218
melib/datetime: isolate unsafe blocks
Isolate unsafe blocks where possible to make code review easier
2021-01-08 15:01:37 +02:00
Manos Pitsidianakis bf9143d8e4
melib/datetime: use Cow<'_, CStr> in timestamp_to_string()
Use Cow to avoid unnecessary allocations when provided a nul-terminated
format string
2021-01-08 15:01:37 +02:00
Manos Pitsidianakis 441dcb62ca
melib/datetime: add format string constants 2021-01-08 15:01:37 +02:00
Manos Pitsidianakis 4cd3e28244
melib/datetime: fix import style inconsistencies 2021-01-08 15:01:37 +02:00
Manos Pitsidianakis 3dba6fdf60
melib/datetime: add posix locale arg in timestamp_to_string() 2021-01-08 15:01:37 +02:00
Manos Pitsidianakis 50cd81772f
melib/jmap: impl watch() with polling
Concerns #22
2021-01-05 19:45:26 +02:00
Manos Pitsidianakis 613c3de3d2
melib/connections: add async sleep(dur: Duration) 2021-01-05 19:45:26 +02:00
Manos Pitsidianakis 62db7d7f32
melib/jmap: put JmapSession behind mutex
And deserialize API urls to Arc<String>.
2021-01-05 17:12:14 +02:00
Manos Pitsidianakis 1c25ae12eb
Use default_cell in CellBuffer resize(), clear() 2021-01-05 17:12:14 +02:00
Manos Pitsidianakis ccc083cf88
Rewrite Cellbuffer Debug impl 2021-01-05 17:12:14 +02:00
Manos Pitsidianakis db69349251
melib/notmuch: avoid parsing entire email in Envelope creation 2021-01-05 17:12:13 +02:00
Manos Pitsidianakis 806254436b
melib/notmuch: add AccountHash field to NotmuchDb 2021-01-05 17:12:12 +02:00
Manos Pitsidianakis 4f164dc700
melib/notmuch: cleanup Query new() method 2021-01-05 17:11:08 +02:00
Manos Pitsidianakis ab0ef1b63c
melib/thread: hash Message-ID for ThreadNodeHash
Make ThreadNodeHash predictable.
2021-01-05 17:11:08 +02:00
Manos Pitsidianakis b966ee8fbd
melib/email: return &mut Self in set_*() methods
Return &mut Self to allow chaining setter methods
2021-01-05 17:11:08 +02:00
Manos Pitsidianakis 34e970d922
melib/datetime: Add Locale struct for error checking 2021-01-04 23:18:55 +02:00
Zisu Andrei f7cbd9a64d
melib/datetime: Set C locale for strptime parsing
This is the locale that should be used for computer interoperability
when doing date and time parsing and formatting.

Without this, on systems which don't have the US locale set, the parsing
returns 0.
2021-01-04 23:18:55 +02:00
Manos Pitsidianakis 829f1243fb
melib/imap: fix invalid FETCH edge case
If mailbox was empty, this FETCH would request "0:*" which is an invalid
message set since message sequence numbers start with 1.
2020-12-30 16:19:15 +02:00
Manos Pitsidianakis 1be30968ca
melib/mbox: fix FreeBSD compilation
Reported in #92
2020-12-29 21:12:38 +02:00
Manos Pitsidianakis 92475c349a
melib/mbox: return Result in file locking 2020-12-29 21:11:52 +02:00
Manos Pitsidianakis 2d5f5e767c
listing/conversations: hash addr by addr_spec in from_address_list
While accumulating addresses for the 'From' list for each envelope
entry, hash the addresses by the address spec (i.e. the email address)
instead of the entire address. This prevents duplicates of the same
email address but with different display names.
2020-12-25 06:10:28 +02:00
Zisu Andrei 0034f195e3
melib/imap: Lazy evaluate idle capability
With the eager evaluation, you run the risk of checking the capabilities
store before any connection to the server may have been opened.
Therefore, the capabilities uid_store will be empty and it will fall
back to poll_with_examine even if the server might have support for
idle.
2020-12-25 06:10:28 +02:00
Manos Pitsidianakis 9124ad0ae7
conf/accounts: remove some unnecessary unwraps 2020-12-25 06:10:28 +02:00
Manos Pitsidianakis ed826357a3
Don't unwrap try_recv() on async jobs channels
Job might have been canceled.
2020-12-25 06:10:28 +02:00
Manos Pitsidianakis b2e853dd7b
melib/imap: update unseen count on \Seen set_flags() 2020-12-24 10:58:31 +02:00
matzipan@gmail.com aa503deb76
melib/imap: Set special usage attributes for INBOX
Without this change, the usage is not correctly identified when calling
imap_mailboxes in the imap backend.
2020-12-24 10:51:57 +02:00
Manos Pitsidianakis fee8f5b575
melib/backends: move IsSubscribedFn to backends mod 2020-12-24 10:50:07 +02:00
Manos Pitsidianakis 7e977fe627
melib/imap/cache/sync: explicitly remove new seen messages from unseen counter 2020-12-24 10:50:07 +02:00
Manos Pitsidianakis 09684e821d
melib/imap: check INBOX when pausing IDLE 2020-12-24 10:50:07 +02:00
Manos Pitsidianakis 10b10e6267
README.md: add mirror links 2020-12-07 15:53:41 +02:00
Manos Pitsidianakis 48e7a493a9
Add reload-config command
Closes #84 Add "reload configuration" command
2020-12-02 21:01:22 +02:00
Manos Pitsidianakis e5b0ff4fe2
state: remove runtime_settings 2020-12-02 21:01:22 +02:00
Manos Pitsidianakis 68f9d1220b
melib/imap: remove DoubleEndedIterator for ImapLineIterator 2020-12-02 17:10:43 +02:00
Manos Pitsidianakis 1408690a9a
melib/imap: don't retry watch conn on non-network error 2020-12-02 17:10:43 +02:00
Manos Pitsidianakis 76814cea20
themes/sail: make only headers_name bold 2020-12-02 17:10:43 +02:00
Manos Pitsidianakis 7e1e57a2df
conf/themes: add mail.view.headers_names and mail.view.headers_area
Allow separate customization of header names and the rest of the header
area.
2020-12-02 17:10:42 +02:00
Manos Pitsidianakis f8a47586e9
mail/listing: show mailbox loading state in status 2020-12-02 17:10:42 +02:00
Manos Pitsidianakis 7efbe6d692
listing: fix menu/sidebar not being redrawn on updates 2020-12-01 20:03:58 +02:00
Manos Pitsidianakis 0f86934e16
mail/status: display in-progress jobs first 2020-12-01 20:03:58 +02:00
Manos Pitsidianakis c5a5c2666b
utilities/pager: show scrolling percentage and/or search results position 2020-12-01 20:03:58 +02:00
Manos Pitsidianakis 7db32ff1b3
terminal/cells: return success flag in CellBuffer::resize() 2020-12-01 01:04:27 +02:00
Manos Pitsidianakis 857d4d546f
utilities/pager: use LineBreakText for lazy line breaking 2020-12-01 01:04:27 +02:00
Manos Pitsidianakis 5327dae02d
melib/text_processing: add LineBreakText iterator
A lazy stateful iterator for line breaking text. Useful for very long text where you don't want to linebreak it completely before user requests specific lines.
2020-12-01 01:04:27 +02:00
Manos Pitsidianakis c990687e5f
docs/meli-themes.5: replace toml spec dead link 2020-12-01 01:04:27 +02:00
Manos Pitsidianakis 453bb0b2b2
melib/smtp: implement gmail XOAUTH2 authentication method 2020-11-30 06:52:16 +02:00
Manos Pitsidianakis 4914f29e20
themes: make conversations defaults grey 2020-11-30 02:20:09 +02:00
Manos Pitsidianakis bedf181aff
melib/imap: examine all mailboxes before idle 2020-11-30 02:20:09 +02:00
Manos Pitsidianakis 9dd21eea50
melib/threads: prefer local ThreadNode env_hash
When inserting an envelope in a thread and its Message-ID already exists
with an associated envelope, overwrite the association if the previous
associated envelope is from a foreign mailbox and current envelope is
not. This happens when mail from a sent folder has been inserted in eg
your INBOX, but somehow INBOX has a copy of your own message as well.
This can happen when mailing lists that send you copies of your own
posts.

The problem with this was that in IMAP your mailing list copy was unseen
and you could not mark it seen because the thread only knew about your
Sent mailbox copy.
2020-11-30 02:20:09 +02:00
Manos Pitsidianakis 4939a1ad9e
melib/imap: remove some debug prints 2020-11-30 02:20:09 +02:00
Manos Pitsidianakis 8e7583a32f
melib/imap: don't clear mailbox counts before fetching 2020-11-30 02:20:09 +02:00
Manos Pitsidianakis 5f6b4745b8
melib/imap: don't use UNSEEN select response for unseen count
UNSEEN field in SELECT/EXAMINE response is meant to be the message
sequence number of the first unseen message, not the count of unseen
messages.
2020-11-30 02:20:08 +02:00
Manos Pitsidianakis 76c1c1a213
melib/imap: don't examine unloaded mailboxes for updates
In examine_updates() which is periodically executed in the IMAP watch
thread, the mailbox's contents get fetched regardless if the user has
fetched the mailbox before. That means eg a large mailbox that was
unused by the user might perform a large fetch all of a sudden without
the user's knowledge/touch.

Add `warm` property in ImapMailbox that states whether the mailbox has
been loaded before in current execution.

Closes #88 IMAP: don't examine unloaded mailboxes for updates, just for message count stats
2020-11-30 02:20:08 +02:00
Manos Pitsidianakis ddfadc748d
melib/imap: don't fetch RFC822 except when requested
In some cases when handling new server events, the entire body message
was unnecessarily fetched.

Closes #87 IMAP: don't fetch RFC822 except when requested
2020-11-30 02:20:08 +02:00
Manos Pitsidianakis 66dea9148b
mail/view: don't update() if coordinates are unchanged 2020-11-29 00:54:27 +02:00
Manos Pitsidianakis 7b3fb86483
mail/view: reset self.theme_default on loading envelope
self.theme_default might have initial value from MailView::default()
which does not correspond to actual theme_default
2020-11-28 20:33:14 +02:00
Manos Pitsidianakis d8c978ed2d
mail/view/thread: fix scrollbar incorrect rendering 2020-11-28 20:33:14 +02:00
Manos Pitsidianakis d076ff573f
MailView, StatusBar: Fix area bound check 2020-11-28 20:33:14 +02:00
Manos Pitsidianakis 6cbb89a8e5
utilities/widgets: fix tiny scrollbar grievances
- set minimum width/height to 1
 - set reverse terminal attribute on !use_color
 - use < > ^ v arrows and # block char if ascii_drawing
2020-11-28 20:33:14 +02:00
Manos Pitsidianakis aa89969dca
utilities: use align_area in shortcut help panel
Before this commit shortcut help panel used to span almost all of the screen.

Use align_area() to center shortcut help box to its minimally required
size.
2020-11-28 20:33:14 +02:00
Manos Pitsidianakis 6a67322570
utilities: add scrollbar on y overflow in shortcuts panel 2020-11-28 20:33:14 +02:00
Manos Pitsidianakis 3e109cabf0
Add sail theme 2020-11-28 20:33:14 +02:00
Manos Pitsidianakis 1cbb6828f2
Add nord theme 2020-11-28 16:33:30 +02:00
Manos Pitsidianakis de018294e4
conf/themes: make notifications bg default color instead of red 2020-11-28 16:33:30 +02:00
Manos Pitsidianakis 6dd3b0bb4f
Fix theme_default not being respected 2020-11-28 16:33:30 +02:00
Manos Pitsidianakis 714ccb5e16
Move Color to src/terminal/color.rs 2020-11-28 16:33:30 +02:00
Manos Pitsidianakis 8d9247e9a3
listing: show auto-hide scrollbar in sidebar menu
Setting to turn it off is listing.show_menu_scrollbar.

Concerns #85 Accounts sidebar doesn't scroll
2020-11-28 16:33:10 +02:00
Manos Pitsidianakis b659749880
listing: scroll account sidebar menu
Closes #85 Accounts sidebar doesn't scroll
2020-11-28 16:32:16 +02:00
Manos Pitsidianakis b053aaa145
listing: prevent invalid area in print_account() 2020-11-28 16:03:36 +02:00
Manos Pitsidianakis 883b3e3a4f
mail/view: show multipart/alternative files properly in attachment list
Show entire multipart/alternative alternatives in attachment list
instead of only the displayed one, in order for the user to be able to
switch alternatives at will.
2020-11-28 15:59:25 +02:00
Manos Pitsidianakis 98c1ece28d
Update xdg-util dependency to 0.4.0 2020-11-28 15:59:25 +02:00
Manos Pitsidianakis 54b2066f73
mail/view: set dirty after closing ContactSelector 2020-11-25 21:19:22 +02:00
Manos Pitsidianakis 007e6320d5
utilities: respect theme_default in shortcut dialog 2020-11-25 21:19:22 +02:00
Manos Pitsidianakis e01275cd93
utilities/dialogs: add cursot Unfocused state as default 2020-11-25 21:19:22 +02:00
Manos Pitsidianakis 879af75d88
utilities/dialogs: use align_area to create box 2020-11-25 21:19:22 +02:00
Manos Pitsidianakis 6a5bb2e057
Add align_area() and Alignment enum 2020-11-25 21:19:22 +02:00
Manos Pitsidianakis 311c1a8a95
utilities/dialogs: respect theme_default 2020-11-25 21:19:22 +02:00
Manos Pitsidianakis ce5c7848e8
utilities: move dialogs to its own submodule 2020-11-25 21:19:22 +02:00
Andrew Jeffery daee4e46de
Allow configuration of the sidebar divider
This adds the config option listing.sidebar_divider to set the character
used to show the divider (defaults to ' ') along with the corresponding
theme in mail.sidebar_divider which defaults to the default theme.
2020-11-25 15:54:47 +02:00
Manos Pitsidianakis 92c12d3526
melib/imap: implement OAUTH2 authentication 2020-11-24 14:28:28 +02:00
Manos Pitsidianakis 0a8a0c04c8
compose: treat inline message/rfc822 as attachments 2020-11-24 14:28:28 +02:00
Manos Pitsidianakis ede5851baf
utilities: reverse order of drawing fields in form
Reverse order of drawing since a field might have an auto complete
prompt below it, so rendering the field below instead of above next
would overwrite it.
2020-11-24 14:28:28 +02:00
Manos Pitsidianakis 79345b3e84
utilities/StatusBar: fix lack of bounds checking in hist_area 2020-11-24 14:28:28 +02:00
Manos Pitsidianakis b46cd09ca6
compose: pass body text when replying
Get rendered body text when creating a new reply Composer instead of
rendering the text in the Composer constructor.

Closes #86
2020-11-24 10:36:31 +02:00
Manos Pitsidianakis bf56c88918
compose: respect auto_choose_multipart_alternative when rendering multipart/alternative attachments to text 2020-11-24 10:36:31 +02:00
Manos Pitsidianakis 73372ff1e7
compose: add show_comments arg to attachment_displays_to_text()
Toggle display of attachment comments (for example "this html attachment
was rendered with X filter...") when rendering text.
2020-11-24 10:36:21 +02:00
Manos Pitsidianakis d4f508642a
widgets: allow text overflow in text fields
Show text content of a text field that exceeds the visible width
properly.
2020-11-24 10:36:21 +02:00
Manos Pitsidianakis f69f623818
Fix some invalid area calculations 2020-11-24 02:23:07 +02:00
Manos Pitsidianakis 2ef2add67f
imap: fix untrimmed query str resulting in invalid search criteria in cyrus 2020-11-24 02:18:41 +02:00
Manos Pitsidianakis 458209b448
view/thread: clear empty space in draw_list 2020-11-24 02:18:41 +02:00
Manos Pitsidianakis b7c48a1ed0
view/thread: make list draw area consistent 2020-11-24 02:18:41 +02:00
Manos Pitsidianakis f25f93fccf
utilities: Fix incorrect calculations in ScrollBar 2020-11-24 02:18:31 +02:00
Manos Pitsidianakis 31e4ed006d
listing: fix off by one error in PageDown movement 2020-11-24 02:18:31 +02:00
Manos Pitsidianakis 179ed52add
compose: grey embed area when embed is stopped
When stopping the embedded terminal with Ctrl-Z or SIGSTOP, show the
terminal area greyed out with a message box.
2020-11-24 02:18:21 +02:00
Manos Pitsidianakis ebc290cc2a
compose: set format flowed if configured in pager 2020-11-24 02:18:21 +02:00
Manos Pitsidianakis f9ce5327c2
melib/imap: fix some LazyCountSet logic errors in sync 2020-11-24 02:18:21 +02:00
Manos Pitsidianakis 5b86c342fb
Update smallvec dependency to 1.5.0
Fixes panicking when loading cached serialized email from older versions
of meli.

https://github.com/servo/rust-smallvec/pull/238
2020-11-22 06:24:38 +02:00
Manos Pitsidianakis 0aa5cf273f
mail/status: don't overwrite "In-progress jobs header" 2020-11-21 02:09:39 +02:00
Manos Pitsidianakis 041257f9a6
melib/text_processing: fix CodePointsIterator implementation
Old implementation was redundant and broken.
2020-11-21 02:09:18 +02:00
Manos Pitsidianakis 1da6d75b08
melib/text_processing: add new wcwidth implementation
Download and parse Unicode data files to judge code point width.
Inspired by https://github.com/ridiculousfish/widecharwidth/
2020-11-21 02:09:18 +02:00
Manos Pitsidianakis a7c0bca8ce
Fix test errors and warnings 2020-11-21 02:09:18 +02:00
Manos Pitsidianakis 023afbaae3
RateLimit: remove unupdated test 2020-11-16 00:45:18 +02:00
Manos Pitsidianakis 1c62de57ae
Never return true on processing JobFinished
JobFinished events are not meant to be inhibited.
2020-11-15 21:30:54 +02:00
Manos Pitsidianakis 76f8bdc558
Add configurable shortcut for 'quit'
Quit ('q' button) was hardcoded, switch to configurable shortcut setting
instead.
2020-11-15 21:30:54 +02:00
Manos Pitsidianakis d404910a0f
melib/smtp: impl AUTH LOGIN
AUTH LOGIN is deprecated but predictably still around.
2020-11-15 21:30:54 +02:00
Manos Pitsidianakis c0e3e78940
listing: dont overdraw menu over listing 2020-11-15 21:30:54 +02:00
Manos Pitsidianakis aaee6d094c
Fix NO_COLOR cursor highlight in sidebar and progress spinner 2020-11-12 03:19:56 +02:00
Manos Pitsidianakis 60350eaa88
mail/status: add "general" shortcut section 2020-11-12 03:19:56 +02:00
Manos Pitsidianakis aa73bd71c3
listing: fix mailbox tree rendering
Indentation value was being interpreted mirrored (raw binary value in
parenthesis):

   0  testing_account (0)
   1   ┣━Archives     (0)
   2   ┃ ┣━2014       (1)
   3   ┃ ┃ ┗━10       (11)
   4   ┃ ┗━2015       (1)
   5     ┃ ┗━05       (10) <- invalid/mirrored
   6   ┣━Drafts       (0)

Should be:

   0  testing_account (0)
   1   ┣━Archives     (0)
   2   ┃ ┣━2014       (1)
   3   ┃ ┃ ┗━10       (11)
   4   ┃ ┗━2015       (1)
   5   ┃   ┗━05       (10)
   6   ┣━Drafts       (0)
2020-11-11 17:14:34 +02:00
Manos Pitsidianakis aa7ebf2918
melib/smtp: fix SMTP syntax error on DSN notify use 2020-11-10 20:30:50 +02:00
Manos Pitsidianakis 2544f54107
melib/compose: prevent bare newlines in finalised drafts 2020-11-10 17:26:06 +02:00
Manos Pitsidianakis 72084da185
Add store_sent_mail option for gmail
- store_sent_mail boolean

Store sent mail after successful submission.
This setting is meant to be disabled for non-standard behaviour in
gmail, which auto-saves sent mail on its own.
2020-11-09 22:22:11 +02:00
Manos Pitsidianakis 23777171f2
listing: clear_area in draw_menu
Completely clear area in draw_menu instead of resetting ch, fg, bg etc.
2020-11-09 19:45:09 +02:00
Manos Pitsidianakis cbaf21764c
Remove status tab, move account status page to listing 2020-11-09 19:35:47 +02:00
Manos Pitsidianakis da69eecafe
mail/status: make AccountStatus public
And fix areas passed to write_string_to_grid() to have the same y
coordinate in upper_left and bottom_right part.
2020-11-09 18:44:56 +02:00
Manos Pitsidianakis f0800f38a8
melib/maildir: make MaildirOp return Result<PathBuf> 2020-11-09 03:36:40 +02:00
Manos Pitsidianakis a34f0aac5b
melib: fix bincode serialization
Previous commit changed bincode deserializes in maildir and sqlite3.rs
from bincode::deserialize_from to using bincode::config::DefaultOptions
and bincode::Options trait's method deserialize_from.

However, these two different deserializes use a different default
settings: https://github.com/servo/bincode/issues/348

Specifically, varint encoding for integers is the default for
DefaultOptions but not when using bincode::{de,}serialize_* functions.
That means that serialized structs were not able to be deserialized.
This commit makes all {de,}serializations use the DefaultOptions
interface rather than the top level functions.
2020-11-09 00:40:32 +02:00
Manos Pitsidianakis 353ac2d029
melib: set upper limit for bincode deserialize
If struct memory layout changes, bincode deserialize fails with memory
allocation error of an obscene amount of bytes. Set upper limit to
deserialized bytes in each place deserialize happens.
2020-11-06 19:05:09 +02:00
Manos Pitsidianakis 6c07046b66
Update bincode dependency to 1.3.1 2020-11-06 18:38:18 +02:00
Manos Pitsidianakis 8ac5558d65
Makefile: add CARGO_ARGS env var
Intended for use with cross-arch compilation flags (--target etc).
2020-11-05 21:11:27 +02:00
Manos Pitsidianakis 43d3d3681e
Makefile: replace install(1) with mkdir, rm, cp, chmod
install(1) is missing in some systems, so replace it with POSIX tools.

Closes #83.
2020-11-05 21:09:42 +02:00
Rudi Horn f1bdae65ee
melib/jmap: add HTTP redirect policy to client
Meli currently uses the .well-known/jmap URL and the RFC8620 requires that any redirects are followed (https://tools.ietf.org/html/rfc8620#section-2.2). This small change allows redirects to happen.
2020-11-04 20:07:51 +02:00
Manos Pitsidianakis 6cc43540d6
docs/meli.conf.5: add SmtpPassword examples 2020-10-30 22:40:56 +02:00
Manos Pitsidianakis 6392904047
Replace PosixTimer with async timers 2020-10-29 13:18:36 +02:00
Manos Pitsidianakis 57e6cf3980
Limit dbus dependency to target_os = "linux" 2020-10-28 23:28:41 +02:00
Manos Pitsidianakis 9a9c876f4a
melib: add more encodings
Add more encodings already supported by `encoding` crate:

  - iso-8859-3,
  - iso-8859-4,
  - iso-8859-5,
  - iso-8859-6,
  - iso-8859-8,
  - iso-8859-10,
  - iso-8859-13,
  - iso-8859-14,
  - iso-8859-16,
  - gb-2312
  - big-5
  - iso-2022-jp
  - euc-jp
  - koi8-r
  - koi8-u
  - utf-16
2020-10-26 22:26:29 +02:00
Manos Pitsidianakis afa74ccfb5
compose: add From text entry autocomplete 2020-10-24 14:36:39 +03:00
Manos Pitsidianakis 560771b32a
widgets: select AutoCompleteEntry on Enter 2020-10-24 14:32:02 +03:00
Manos Pitsidianakis 7b1ab389fa
Remove unused plugin interface 2020-10-21 17:58:30 +03:00
Manos Pitsidianakis 594a2bd0dd
listing: add set operations to range select actions
Add symmetric difference (default), union, difference and intersection
modifiers for selecting ranges. That way you can quickly construct the
selection set you need.
2020-10-21 14:36:51 +03:00
Manos Pitsidianakis 05ef863a45
utilities: move PageMovement to components mod 2020-10-21 13:19:13 +03:00
Manos Pitsidianakis d5aa2cb3ef
melib/line_break: add segment tree impl
The widths of subslices of a line are calculated in each call to
`binary_search_by` when reflowing long lines. This can be done in Ologn
queries with a segment tree.
2020-10-20 23:53:00 +03:00
Manos Pitsidianakis f7fc2e31e0
melib: Remove unused crossbeam dependency 2020-10-20 23:30:29 +03:00
Manos Pitsidianakis 00f5c4b9c0
melib/maildir: split parsing into big chunks 2020-10-20 23:27:10 +03:00
Manos Pitsidianakis 4b91de3d59
state: remove overlay widgets on ComponentKill events 2020-10-20 23:19:13 +03:00
Manos Pitsidianakis eb36034740
accounts: autoload Sent folders automatically 2020-10-20 23:18:27 +03:00
Manos Pitsidianakis d4e347289c
melib/README: update feature table 2020-10-20 23:15:52 +03:00
Manos Pitsidianakis 662706607b
melib: remove memmap dependency
It's unmaintained, and the IO performance gains are negligible
2020-10-20 22:41:44 +03:00
Manos Pitsidianakis b904f91f45
README: replace svg with webp screenshots
Gitea doesn't render svg images (delivers them as text/plain)
2020-10-20 22:13:02 +03:00
Manos Pitsidianakis 9f39a7c5a1
statusbar: delete num buffer chars with Backspace 2020-10-20 15:09:00 +03:00
Manos Pitsidianakis 126ed8a189
statusbar: don't overwrite num buffer when progress spinner is deactivated 2020-10-20 15:04:50 +03:00
Manos Pitsidianakis 91fe7435f7
melib/imap: add suggestion on STARTTLS error
If server port is 993 (IMAPS) and starttls is enabled, suggest turning
it off if starttls fails.
2020-10-20 14:58:16 +03:00
Manos Pitsidianakis 7a9c150f33
melib/imap: fetch References header along with ENVELOPE
Threading was broken if information was needed from References header.
For example, mailman might alter some Message-IDs when using its NNTP
bridge and the complete references are necessary to rebuild the thread,
which is only available in References whereas ENVELOPE has only
In-Reply-To.
2020-10-18 17:42:54 +03:00
Manos Pitsidianakis b9f4d718c7
melib/sqlite3: reset db on version mismatch 2020-10-18 17:41:06 +03:00
Manos Pitsidianakis 54cb4ea623
melib/build.rs: remove unnecessary file creation 2020-10-18 15:34:09 +03:00
Manos Pitsidianakis 7919e95ddd
terminal/embed: remove some allocations and unwraps 2020-10-18 15:02:18 +03:00
Manos Pitsidianakis 89940dd606
cli-docs: compress included text 2020-10-17 20:50:29 +03:00
Manos Pitsidianakis b69bc219c3
README.md: Add screenshots and update text 2020-10-17 15:02:38 +03:00
Manos Pitsidianakis bb51d36579
composer: send NewJob event on submission 2020-10-16 22:30:56 +03:00
Manos Pitsidianakis a2456fa3f5
docs/meli.conf.5: small fixes & additions 2020-10-16 22:28:00 +03:00
Manos Pitsidianakis 3b97e66c10
docs/meli.conf.5: add progress_spinner_sequence doc 2020-10-16 15:47:00 +03:00
Manos Pitsidianakis ddfec3e207
listing: fix menu draw artifact 2020-10-16 15:46:21 +03:00
Manos Pitsidianakis a702a04043
melib/attachments: add SMIME signature variant 2020-10-16 12:47:16 +03:00
Manos Pitsidianakis 6264ee011f
terminal/embed: remove unwraps from kill() calls
If child process has exited, this will panic.
2020-10-16 12:41:21 +03:00
Manos Pitsidianakis 5acd7dfe1c
mail/view: prevent spurious redraw in special modes 2020-10-16 12:40:02 +03:00
Manos Pitsidianakis 8090d614e2
conf/pager: remove unused max_width option 2020-10-16 12:37:46 +03:00
Manos Pitsidianakis 3949cecb75
mail/composer: add scrollbars 2020-10-16 12:37:01 +03:00
Manos Pitsidianakis 1e7b40e6b3
utilities: move pager to its own module 2020-10-15 22:44:15 +03:00
Manos Pitsidianakis d8d66641e2
utilities/widgets: only advance stage by timer in ProgressSpinner 2020-10-15 21:45:12 +03:00
Manos Pitsidianakis 393c5d0d53
state: cull redraws of floating notifications
Cull redraws by keeping track of whether the floating box has been
initialised and whether its area has been drawn over by other dirty areas.
2020-10-15 21:28:28 +03:00
Manos Pitsidianakis 4c1a9b2485
Fix minor warnings 2020-10-15 19:01:42 +03:00
Manos Pitsidianakis 03a1d5a985
listing: Update status on all update events
Envelope counter totals might remain stale in the bottom status bar.
2020-10-15 19:00:37 +03:00
Manos Pitsidianakis 279c288a22
Alter enter_command_mode shortcut to `:`
Just like vi.
2020-10-14 20:21:22 +03:00
Manos Pitsidianakis e4cddbad25
mail/view: send NewJob event on new jobs
NewJob event wasn't sent so the message loading jobs were not accounted
in the busy spinner animation
2020-10-14 20:16:54 +03:00
Manos Pitsidianakis 67f50d95f4
Add quit command 2020-10-14 20:14:07 +03:00
Manos Pitsidianakis 0c68807814
Add export-mail command 2020-10-14 20:13:15 +03:00
Manos Pitsidianakis 4e72b6552a
conf: add setting for progress spinner
Choose between 30-something built in sequences (integers between 0-30)
or define your own list of strings for the progress spinner animation.

Default: 0
2020-10-14 20:07:39 +03:00
Manos Pitsidianakis 310d02042f
Rename toggle_thread_snooze to "toggle thread_snooze"
For consistency with other toggle commands.
2020-10-14 14:58:02 +03:00
Manos Pitsidianakis 188e020bd1
Add opt-in mouse support
Sidebar width can be resized with mouse hold and drag.
2020-10-14 14:58:02 +03:00
Manos Pitsidianakis 20840625d6
melib/gpgme: trim header file 2020-10-14 01:03:57 +03:00
Manos Pitsidianakis d51d0187a6
melib/imap: change byte cache String -> Vec<u8> 2020-10-13 21:46:03 +03:00
Manos Pitsidianakis 2944fc992b
melib/imap/untagged: handle EXPUNGE if our MSNs invalid 2020-10-13 21:18:26 +03:00
Manos Pitsidianakis 535d04f4f0
melib/imap/untagged: lower mbox count on EXPUNGE events 2020-10-13 21:17:27 +03:00
Manos Pitsidianakis 6f31388b27
compose: add EditAttachments menu 2020-10-13 17:17:57 +03:00
Manos Pitsidianakis 5337a54d96
compose: move gpg mod to its own file 2020-10-13 17:06:30 +03:00
Manos Pitsidianakis b343530f0c
widgets: add button type parameter to FormWidget 2020-10-13 17:04:40 +03:00
Manos Pitsidianakis cd68008e67
melib: Implement delete_messages for IMAP, Maildir 2020-10-13 13:57:04 +03:00
Manos Pitsidianakis 19891a3042
Cargo.toml: set codegen-units = 1 2020-10-11 18:11:04 +03:00
Manos Pitsidianakis 9ce62c735a
compose: add key selection state for gpg operations
Closes #81
2020-10-11 18:11:04 +03:00
Manos Pitsidianakis 39fab67523
compose: use melib::Bytes pretty print for attachment size 2020-10-11 16:53:05 +03:00
Manos Pitsidianakis 0ca7b0042e
utilities: ensure Form/Button widgets are not always non-dirty 2020-10-11 16:53:04 +03:00
Manos Pitsidianakis 406af1848f
compose: add `add-attachment-file-picker` command 2020-10-11 16:53:04 +03:00
Manos Pitsidianakis a4b78532b7
Refactor job structs into JoinHandle
Put oneshot::channel<R> into JoinHandle<R>
2020-10-11 16:53:04 +03:00
Manos Pitsidianakis 4dd8474c30
gpgme: add PartialEq impl for Key 2020-10-11 16:53:04 +03:00
Manos Pitsidianakis 0dd9e6a34b
compose: kill selectors on ComponentKill 2020-10-11 16:53:04 +03:00
Manos Pitsidianakis eb1cb5cec6
compose: expand cursor reach to attachment area 2020-10-11 16:53:04 +03:00
Manos Pitsidianakis e42c9281fd
Fix input events going to hidden components 2020-10-11 16:53:04 +03:00
Manos Pitsidianakis bc74379b27
mailview: don't process_event if coordinates uninitialised 2020-10-11 16:53:04 +03:00
Manos Pitsidianakis be45b0c02d
compose: add encrypt layer 2020-10-11 16:53:04 +03:00
Manos Pitsidianakis 3ec1ecb349
Add import mail action 2020-10-11 16:53:04 +03:00
Manos Pitsidianakis afe7eed9ef
melib/compose: don't base64 encode unless it's not ascii 2020-10-11 16:53:04 +03:00
Manos Pitsidianakis 59e60f8d28
gpgme: add context flag set/get 2020-10-11 16:53:04 +03:00
Manos Pitsidianakis a2f11c341d
compose: add async draft filter stack in sending mail
Add a stack of "filter" closures that edit a draft before sending it.
Add PGP signing filter. An encryption filter will be added in a future
commit.
2020-10-11 16:53:04 +03:00
Manos Pitsidianakis afee1e2be5
melib/compose: fix wrong Content-Type on PGP signatures and message/rfc822 2020-10-11 16:53:04 +03:00
Manos Pitsidianakis 08df7f39b2
Add toggle encrypt action in composer
Does nothing for now, will be used in a future commit.
2020-10-11 16:53:04 +03:00
Manos Pitsidianakis 5d968b7c40
imap: fix out of bounds panic on receive EXPUNGE
Closes #82
2020-10-11 16:53:04 +03:00
Manos Pitsidianakis 347b54e0f7
segment_tree: get_max() return 0 if tree empty 2020-10-05 21:10:00 +03:00
Manos Pitsidianakis 74f31875b8
listing: fix menu gaining focus if not visible 2020-10-05 21:10:00 +03:00
Manos Pitsidianakis 23ca41e3e8
add libgpgme feature 2020-10-05 21:10:00 +03:00
Manos Pitsidianakis b9c07bacef
melib: decode text inline message/rfc822 attachments 2020-09-27 20:57:42 +03:00
Manos Pitsidianakis 87443f156f
docs/meli.1: add copyto, moveto, delete commands 2020-09-26 18:18:24 +03:00
Manos Pitsidianakis b0e50a29bd
melib/list_management: don't ignore "NO" in List-Post 2020-09-25 13:45:48 +03:00
Manos Pitsidianakis 1ddde400ee
debian/: bump version to 0.6.2 2020-09-24 18:15:46 +03:00
Manos Pitsidianakis 6ccb4e9544
melib: bump version to 0.6.2 2020-09-24 17:13:07 +03:00
Manos Pitsidianakis e407b1e224
melib: add README.md and email module doco 2020-09-24 16:54:06 +03:00
Manos Pitsidianakis a1e3f269de
melib/imap: don't manually check for mailbox permissions 2020-09-24 12:17:32 +03:00
Manos Pitsidianakis e556191bab
melib/imap: hide LOGIN from debug log 2020-09-24 12:16:50 +03:00
Manos Pitsidianakis ce559b05d7
melib/imap: EXAMINE instead of SELECT in IDLE connection 2020-09-24 12:15:00 +03:00
Manos Pitsidianakis 36cc0d4212
melib/jmap: implement refresh()
Closes #77
2020-09-23 10:52:19 +03:00
Manos Pitsidianakis 425f4b9930
melib/jmap: add Type parameter to Id, State
Make Id, State have a type parameter to the object it refers to (eg
`Id<EmailObject>`) instead of just a String
2020-09-23 10:52:19 +03:00
Manos Pitsidianakis 19d4a191d8
melib/jmap: add email state sync 2020-09-21 16:17:37 +03:00
Manos Pitsidianakis 20dd4cfaf6
Makefile: fix error with manpage path 2020-09-20 23:10:46 +03:00
Manos Pitsidianakis 4cf0b9ffec
melib/jmap: impl copy_messages()
Closes #76
2020-09-20 15:00:03 +03:00
Manos Pitsidianakis 559de5e140
Add docs/ folder 2020-09-20 15:00:03 +03:00
Manos Pitsidianakis baa44109f2
melib/thread: "merge" duplicate messages in threads 2020-09-20 15:00:03 +03:00
Manos Pitsidianakis 28deba708c
melib/imap: check if FETCH reply was intended for us
After sending a FETCH, the command results might be mixed with
unsolicited FETCH replies. Check if that happens.
2020-09-20 15:00:03 +03:00
Manos Pitsidianakis a187cee1d3
plugins: place socket in XDG_RUNTIME_DIR, not CWD
Closes #78
2020-09-20 13:31:18 +03:00
Manos Pitsidianakis ea0fb114e1
melib/imap: delete reverse_modseq storage
Modsequences are not unique, and many messages may share the same
modsequence. So storing a reverse mapping of modsequences to messages is
invalid.
2020-09-20 13:29:57 +03:00
Manos Pitsidianakis 8e036f045c
melib/imap: accept literal astrings in bodystructure 2020-09-19 22:54:11 +03:00
Manos Pitsidianakis 3210ee5c67
melib/jmap: impl save() message
Closes #60
2020-09-19 20:44:39 +03:00
Manos Pitsidianakis cfc380b47d
melib/jmap: allow empty to,from etc fields in EmailObject 2020-09-19 14:59:23 +03:00
Manos Pitsidianakis fba69d1e5d
SearchBackend: add Auto variant as default 2020-09-18 21:38:50 +03:00
Manos Pitsidianakis 7dfa6c0639
view/thread: use reverse colors in cursor in case of NO_COLOR 2020-09-18 21:28:41 +03:00
Manos Pitsidianakis 82cd690005
sqlite3: only update when SearchBackend is sqlite3 2020-09-18 21:06:34 +03:00
Manos Pitsidianakis 8eb78ae01b
sidebar: compute mailbox tree only for subscribed mailboxes 2020-09-18 21:06:33 +03:00
Manos Pitsidianakis 05e4dbcd5a
melib: update smol to 1.0.0 2020-09-18 21:06:33 +03:00
Manos Pitsidianakis 40b63cc3e0
melib/imap: fix unseen count on cache sync 2020-09-18 12:21:05 +03:00
Manos Pitsidianakis 38eff71971
IMAP: don't show \Recent flag as tag
Closes #74
2020-09-18 12:12:14 +03:00
Manos Pitsidianakis 3004789f32
melib/imap: FETCH comma-sep list on untagged Recent response
FETCHing RECENT messages when receiving an untagged RECENT response from
the server didn't separate the message numbers with comma but with
space, which is invalid.
2020-09-18 12:10:44 +03:00
Manos Pitsidianakis 9bafba3905
melib/imap: don't print raw bytes in debug prints 2020-09-18 12:08:56 +03:00
Manos Pitsidianakis 98949a4a72
melib/imap: expand special mailbox detection cases 2020-09-18 12:08:02 +03:00
145 changed files with 26272 additions and 9122 deletions

View File

@ -7,6 +7,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Added
- Add import command to import email from files into accounts
- Add add-attachment-file-picker command and `file_picker_command` setting to
use external commands to choose files when composing new mail
## [alpha-0.6.2] - 2020-09-24
### Added
- Add customizable mailbox tree in sidebar
- Make `dbus` dependency opt-out (feature is `dbus-notifications`)
- Implemented JMAP async, search, tagging, syncing
- Preserve account order from configuration file
- Implemented IMAP `CONDSTORE` support for IMAP cache
- Add `timeout` setting for IMAP
- Implement TCP keepalive for IMAP
- Rewrote email address parsers.
- Implement `copy_messages` for maildir
- Implement selection with motions
### Fixed
- Fixed various problems with IMAP cache
- Fixed various problems with IMAP message counts
- Fixed various problems with IMAP connection hanging
- Fixed IMAP not reconnecting on dropped IDLE connections
- Fixed various problems with notmuch backend
## [alpha-0.6.1] - 2020-08-02
### Added
@ -82,3 +108,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
[alpha-0.5.1]: https://github.com/meli/meli/releases/tag/alpha-0.5.1
[alpha-0.6.0]: https://github.com/meli/meli/releases/tag/alpha-0.6.0
[alpha-0.6.1]: https://github.com/meli/meli/releases/tag/alpha-0.6.1
[alpha-0.6.2]: https://github.com/meli/meli/releases/tag/alpha-0.6.2

884
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
[package]
name = "meli"
version = "0.6.1"
version = "0.6.2"
authors = ["Manos Pitsidianakis <el13635@mail.ntua.gr>"]
edition = "2018"
@ -31,25 +31,21 @@ crossbeam = "0.7.2"
signal-hook = "0.1.12"
signal-hook-registry = "1.2.0"
nix = "0.17.0"
melib = { path = "melib", version = "0.6.1" }
melib = { path = "melib", version = "0.6.2" }
serde = "1.0.71"
serde_derive = "1.0.71"
serde_json = "1.0"
toml = { version = "0.5.6", features = ["preserve_order", ] }
indexmap = { version = "^1.5", features = ["serde-1", ] }
indexmap = { version = "^1.6", features = ["serde-1", ] }
linkify = "0.4.0"
notify = "4.0.1" # >:c
notify-rust = { version = "^4", optional = true }
termion = "1.5.1"
bincode = "1.2.0"
bincode = "^1.3.0"
uuid = { version = "0.8.1", features = ["serde", "v4"] }
unicode-segmentation = "1.2.1" # >:c
libc = {version = "0.2.59", features = ["extra_traits",]}
rmp = "^0.8"
rmpv = { version = "^0.4.2", features=["with-serde",] }
rmp-serde = "^0.14.0"
smallvec = { version = "^1.4.0", features = ["serde", ] }
smallvec = { version = "^1.5.0", features = ["serde", ] }
bitflags = "1.0"
pcre2 = { version = "0.2.3", optional = true }
structopt = { version = "0.3.14", default-features = false }
@ -57,30 +53,37 @@ svg_crate = { version = "0.8.0", optional = true, package = "svg" }
futures = "0.3.5"
async-task = "3.0.0"
num_cpus = "1.12.0"
flate2 = { version = "1.0.16", optional = true }
[target.'cfg(target_os="linux")'.dependencies]
notify-rust = { version = "^4", optional = true }
[build-dependencies]
syn = { version = "1.0.31", features = [] }
quote = "^1.0"
proc-macro2 = "1.0.18"
flate2 = { version = "1.0.16", optional = true }
[profile.release]
lto = "fat"
opt-level = "z"
codegen-units = 1
opt-level = "s"
debug = false
[workspace]
members = ["melib", "tools", ]
[features]
default = ["sqlite3", "notmuch", "regexp", "smtp", "dbus-notifications"]
default = ["sqlite3", "notmuch", "regexp", "smtp", "dbus-notifications", "gpgme"]
notmuch = ["melib/notmuch_backend", ]
jmap = ["melib/jmap_backend",]
sqlite3 = ["melib/sqlite3"]
smtp = ["melib/smtp"]
regexp = ["pcre2"]
dbus-notifications = ["notify-rust",]
cli-docs = []
cli-docs = ["flate2"]
svgscreenshot = ["svg_crate"]
gpgme = ["melib/gpgme"]
# Print tracing logs as meli runs in stderr
# enable for debug tracing logs: build with --features=debug-tracing

View File

@ -26,8 +26,10 @@ MANDIR ?= ${EXPANDED_PREFIX}/share/man
CARGO_TARGET_DIR ?= target
MIN_RUSTC ?= 1.39.0
CARGO_BIN ?= cargo
CARGO_ARGS ?=
# Installation parameters
DOCS_SUBDIR ?= docs/
MANPAGES ?= meli.1 meli.conf.5 meli-themes.5
FEATURES ?= --features "${MELI_FEATURES}"
@ -47,7 +49,7 @@ GREEN ?= `[ -z $${NO_COLOR+x} ] && ([ -z $${TERM} ] && echo "" || tput setaf 2)
.POSIX:
.SUFFIXES:
meli: check-deps
@${CARGO_BIN} build ${CARGO_COLOR}--target-dir="${CARGO_TARGET_DIR}" ${FEATURES} --release
@${CARGO_BIN} build ${CARGO_ARGS} ${CARGO_COLOR}--target-dir="${CARGO_TARGET_DIR}" ${FEATURES} --release
help:
@echo "For a quick start, build and install locally:\n ${BOLD}${GREEN}PREFIX=~/.local make install${ANSI_RESET}\n"
@ -83,15 +85,18 @@ help:
@echo -n "* NO_COLOR ${UNDERLINE}"
@[ $${NO_COLOR+x} ] && echo -n "set" || echo -n "unset"
@echo ${ANSI_RESET}
@echo "* CARGO_BIN = ${UNDERLINE}${CARGO_BIN}${ANSI_RESET}"
@echo "* CARGO_ARGS = ${UNDERLINE}${CARGO_ARGS}${ANSI_RESET}"
@echo "* MIN_RUSTC = ${UNDERLINE}${MIN_RUSTC}${ANSI_RESET}"
@#@echo "* CARGO_COLOR = ${CARGO_COLOR}"
.PHONY: check
check:
@${CARGO_BIN} test ${CARGO_COLOR}--target-dir="${CARGO_TARGET_DIR}" --workspace
@${CARGO_BIN} test ${CARGO_ARGS} ${CARGO_COLOR}--target-dir="${CARGO_TARGET_DIR}" --workspace
.PHONY: check-deps
check-deps:
@(if ! echo ${MIN_RUSTC}\\n`${CARGO_BIN} --version | cut -d ' ' -f 2` | sort -CV; then echo "rust version >= ${RED}${MIN_RUSTC}${ANSI_RESET} required, found: `which ${CARGO_BIN}` `${CARGO_BIN} --version | cut -d ' ' -f 2`" \
@(if ! echo ${MIN_RUSTC}\\n`${CARGO_BIN} --version | grep ^cargo | cut -d ' ' -f 2` | sort -CV; then echo "rust version >= ${RED}${MIN_RUSTC}${ANSI_RESET} required, found: `which ${CARGO_BIN}` `${CARGO_BIN} --version | cut -d ' ' -f 2`" \
"\nYour options:\n - Set CARGO_BIN to a supported version\n - Install a supported version from your distribution's package manager\n - Install a supported version from ${UNDERLINE}https://rustup.rs/${ANSI_RESET}" ; exit 1; fi)
@ -120,7 +125,7 @@ install-doc:
SECTION=`echo $${MANPAGE} | rev | cut -d "." -f 1`; \
MANPAGEPATH=${DESTDIR}${MANDIR}/man$${SECTION}/$${MANPAGE}.gz; \
echo " * installing $${MANPAGE} → ${GREEN}$${MANPAGEPATH}${ANSI_RESET}"; \
gzip -n < $${MANPAGE} > $${MANPAGEPATH} \
gzip -n < ${DOCS_SUBDIR}$${MANPAGE} > $${MANPAGEPATH} \
; done ; \
(case ":${MANPATHS}:" in \
*:${DESTDIR}${MANDIR}:*) echo -n "";; \
@ -136,7 +141,10 @@ install-bin: meli
*:${DESTDIR}${BINDIR}:*) echo -n "";; \
*) echo "\n${RED}${BOLD}WARNING${ANSI_RESET}: ${UNDERLINE}Path ${DESTDIR}${BINDIR} is not contained in your PATH variable.${ANSI_RESET} Consider adding it if necessary.\nPATH variable: ${PATH}";; \
esac
@install -D ./${CARGO_TARGET_DIR}/release/meli $(DESTDIR)${BINDIR}/meli
@mkdir -p $(DESTDIR)${BINDIR}
@rm -f $(DESTDIR)${BINDIR}/meli
@cp ./${CARGO_TARGET_DIR}/release/meli $(DESTDIR)${BINDIR}/meli
@chmod 755 $(DESTDIR)${BINDIR}/meli
.PHONY: install
@ -160,4 +168,4 @@ deb-dist:
.PHONY: build-rustdoc
build-rustdoc:
@RUSTDOCFLAGS="--crate-version ${VERSION}_${GIT_COMMIT}_${DATE}" ${CARGO_BIN} doc ${CARGO_COLOR}--target-dir="${CARGO_TARGET_DIR}" --all-features --no-deps --workspace --document-private-items --open
@RUSTDOCFLAGS="--crate-version ${VERSION}_${GIT_COMMIT}_${DATE}" ${CARGO_BIN} doc ${CARGO_ARGS} ${CARGO_COLOR}--target-dir="${CARGO_TARGET_DIR}" --all-features --no-deps --workspace --document-private-items --open

138
README.md
View File

@ -1,92 +1,87 @@
# meli
# meli [![GitHub license](https://img.shields.io/github/license/meli/meli)](https://github.com/meli/meli/blob/master/COPYING) [![Crates.io](https://img.shields.io/crates/v/meli)](https://crates.io/crates/meli)
**BSD/Linux terminal email client with support for multiple accounts and Maildir / mbox / notmuch / IMAP / JMAP.**
Community links:
[mailing lists](https://lists.meli.delivery/) | `#meli` on OFTC IRC | Report bugs and/or feature requests in [meli's issue tracker](https://git.meli.delivery/meli/meli/issues "meli gitea issue tracker")
| | | |
:---:|:---:|:---:
![Main view screenshot](./docs/screenshots/main.webp "mail meli view screenshot") | ![Compact main view screenshot](./docs/screenshots/compact.webp "compact main view screenshot") | ![Compose with embed terminal editor screenshot](./docs/screenshots/compose.webp "composing view screenshot")
Main view | Compact main view | Compose with embed terminal editor
Main repository:
* https://git.meli.delivery/meli/meli
Official mirrors:
* https://github.com/meli/meli
## Install
- Try an [online interactive web demo](https://meli.delivery/wasm2.html "online interactive web demo") powered by WebAssembly
- [`cargo install meli`](https://crates.io/crates/meli "crates.io meli package")
- [Download and install pre-built debian package, static linux binary](https://github.com/meli/meli/releases/ "github releases for meli"), or
- Install with [Nix](https://search.nixos.org/packages?show=meli&query=meli&from=0&size=30&sort=relevance&channel=unstable#disabled "nixos package search results for 'meli'")
## Documentation
See also [Quickstart tutorial](https://meli.delivery/documentation.html#quick-start).
After installing meli, see `meli(1)`, `meli.conf(5)` and `meli-themes(5)` for documentation. Sample configuration and theme files can be found in the `docs/samples/` subdirectory. Manual pages are also [hosted online](https://meli.delivery/documentation.html "meli documentation").
meli by default looks for a configuration file in this location: `$XDG_CONFIG_HOME/meli/config.toml`
You can run meli with arbitrary configuration files by setting the `$MELI_CONFIG`
environment variable to their locations, i.e.:
```sh
MELI_CONFIG=./test_config cargo run
```
## Build
For a quick start, build and install locally:
```sh
PREFIX=~/.local make install
```
Available subcommands:
- meli (builds meli with optimizations in `$CARGO_TARGET_DIR`)
- install (installs binary in `$BINDIR` and documentation to `$MANDIR`)
- uninstall
Secondary subcommands:
- clean (cleans build artifacts)
- check-deps (checks dependencies)
- install-bin (installs binary to `$BINDIR`)
- install-doc (installs manpages to `$MANDIR`)
- help (prints this information)
- dist (creates release tarball named `meli-VERSION.tar.gz` in this directory)
- deb-dist (builds debian package in the parent directory)
- distclean (cleans distribution build artifacts)
- build-rustdoc (builds rustdoc documentation for all packages in `$CARGO_TARGET_DIR`)
The Makefile *should* be portable and not require a specific `make` version.
# Documentation
After installing meli, see `meli(1)` and `meli.conf(5)` for documentation. Sample configuration and theme files can be found in the `samples/` subdirectory.
# Building
Available subcommands for `make` are listed with `make help`. The Makefile *should* be POSIX portable and not require a specific `make` version.
meli requires rust 1.39 and rust's package manager, Cargo. Information on how
to get it on your system can be found here: <https://doc.rust-lang.org/cargo/getting-started/installation.html>
With Cargo available, the project can be built with
With Cargo available, the project can be built with `make` and the resulting binary will then be found under `target/release/meli`. Run `make install` to install the binary and man pages. This requires root, so I suggest you override the default paths and install it in your `$HOME`: `make PREFIX=$HOME/.local install`.
```sh
make
```
You can build and run meli with one command: `cargo run --release`.
The resulting binary will then be found under `target/release/meli`
Run:
```sh
make install
```
to install the binary and man pages. This requires root, so I suggest you override the default paths and install it in your `$HOME`:
```sh
make PREFIX=$HOME/.local install
```
See `meli(1)` and `meli.conf(5)` for documentation.
You can build and run meli with one command:
```sh
cargo run --release
```
While the project is in early development, meli will only be developed for the
linux kernel and respected linux distributions. Support for more UNIX-like OSes
is on the roadmap.
## Features
### Build features
Some functionality is held behind "feature gates", or compile-time flags. The following list explains each feature's purpose:
- `dbus-notifications` enables showing notifications using `dbus`.
- `notmuch` provides support for using a notmuch database as a mail backend
- `jmap` provides support for connecting to a jmap server and use it as a mail backend
- `sqlite3` provides support for builting fast search indexes in local sqlite3 databases
- `gpgme` enables GPG support via `libgpgme` (on by default)
- `dbus-notifications` enables showing notifications using `dbus` (on by default)
- `notmuch` provides support for using a notmuch database as a mail backend (on by default)
- `jmap` provides support for connecting to a jmap server and use it as a mail backend (off by default)
- `sqlite3` provides support for builting fast search indexes in local sqlite3 databases (on by default)
- `cli-docs` includes the manpage documentation compiled by either `mandoc` or `man` binary to plain text in `meli`'s command line. Embedded documentation can be viewed with the subcommand `meli man [PAGE]`
- `svgscreenshot` provides support for taking screenshots of the current view of meli and saving it as SVG files. Its only purpose is taking screenshots for the official meli webpage.
- `debug-tracing` enables various trace debug logs from various places around the meli code base. The trace log is printed in `stderr`.
- `svgscreenshot` provides support for taking screenshots of the current view of meli and saving it as SVG files. Its only purpose is taking screenshots for the official meli webpage. (off by default)
- `debug-tracing` enables various trace debug logs from various places around the meli code base. The trace log is printed in `stderr`. (off by default)
## Building in Debian
### Build Debian package (*deb*)
Building with Debian's packaged cargo might require the installation of these
two packages: `librust-openssl-sys-dev librust-libdbus-sys-dev`
A `*.deb` package can be built with `make deb-dist`
# Using notmuch
### Using notmuch
To use the optional notmuch backend feature, you must have `libnotmuch5` installed in your system. In Debian-like systems, install the `libnotmuch5` packages. meli detects the library's presence on runtime.
# Building with JMAP
### Using GPG
To use the optional gpg feature, you must have `libgpgme` installed in your system. In Debian-like systems, install the `libgpgme11` package. meli detects the library's presence on runtime.
### Building with JMAP
To build with JMAP support, prepend the environment variable `MELI_FEATURES='jmap'` to your make invocation:
@ -111,18 +106,7 @@ are printed in stderr, thus you can run meli with a redirection (i.e `2> log`)
Code style follows the default rustfmt profile.
# Configuration
meli by default looks for a configuration file in this location: `$XDG_CONFIG_HOME/meli/config.toml`
You can run meli with arbitrary configuration files by setting the `$MELI_CONFIG`
environment variable to their locations, ie:
```sh
MELI_CONFIG=./test_config cargo run
```
# Testing
## Testing
How to run specific tests:
@ -130,14 +114,14 @@ How to run specific tests:
cargo test -p {melib, meli} (-- --nocapture) (--test test_name)
```
# Profiling
## Profiling
```sh
perf record -g target/debug/bin
perf script | stackcollapse-perf | rust-unmangle | flamegraph > perf.svg
```
# Running fuzz targets
## Running fuzz targets
Note: `cargo-fuzz` requires the nightly toolchain.

View File

@ -37,6 +37,8 @@ fn main() {
]);
#[cfg(feature = "cli-docs")]
{
use flate2::Compression;
use flate2::GzBuilder;
const MANDOC_OPTS: &[&'static str] = &["-T", "utf8", "-I", "os=Generated by mandoc(1)"];
use std::env;
use std::fs::File;
@ -45,38 +47,60 @@ fn main() {
use std::process::Command;
let out_dir = env::var("OUT_DIR").unwrap();
let mut out_dir_path = Path::new(&out_dir).to_path_buf();
out_dir_path.push("meli.txt");
out_dir_path.push("meli.txt.gz");
let output = Command::new("mandoc")
.args(MANDOC_OPTS)
.arg("meli.1")
.arg("docs/meli.1")
.output()
.or_else(|_| Command::new("man").arg("-l").arg("meli.1").output())
.or_else(|_| Command::new("man").arg("-l").arg("docs/meli.1").output())
.unwrap();
let mut file = File::create(&out_dir_path).unwrap();
file.write_all(&output.stdout).unwrap();
let file = File::create(&out_dir_path).unwrap();
let mut gz = GzBuilder::new()
.comment(output.stdout.len().to_string().into_bytes())
.write(file, Compression::default());
gz.write_all(&output.stdout).unwrap();
gz.finish().unwrap();
out_dir_path.pop();
out_dir_path.push("meli.conf.txt");
out_dir_path.push("meli.conf.txt.gz");
let output = Command::new("mandoc")
.args(MANDOC_OPTS)
.arg("meli.conf.5")
.arg("docs/meli.conf.5")
.output()
.or_else(|_| Command::new("man").arg("-l").arg("meli.conf.5").output())
.or_else(|_| {
Command::new("man")
.arg("-l")
.arg("docs/meli.conf.5")
.output()
})
.unwrap();
let mut file = File::create(&out_dir_path).unwrap();
file.write_all(&output.stdout).unwrap();
let file = File::create(&out_dir_path).unwrap();
let mut gz = GzBuilder::new()
.comment(output.stdout.len().to_string().into_bytes())
.write(file, Compression::default());
gz.write_all(&output.stdout).unwrap();
gz.finish().unwrap();
out_dir_path.pop();
out_dir_path.push("meli-themes.txt");
out_dir_path.push("meli-themes.txt.gz");
let output = Command::new("mandoc")
.args(MANDOC_OPTS)
.arg("meli-themes.5")
.arg("docs/meli-themes.5")
.output()
.or_else(|_| Command::new("man").arg("-l").arg("meli-themes.5").output())
.or_else(|_| {
Command::new("man")
.arg("-l")
.arg("docs/meli-themes.5")
.output()
})
.unwrap();
let mut file = File::create(&out_dir_path).unwrap();
file.write_all(&output.stdout).unwrap();
let file = File::create(&out_dir_path).unwrap();
let mut gz = GzBuilder::new()
.comment(output.stdout.len().to_string().into_bytes())
.write(file, Compression::default());
gz.write_all(&output.stdout).unwrap();
gz.finish().unwrap();
}
}

View File

@ -87,6 +87,14 @@ use super::*;
}
let override_ident: syn::Ident = format_ident!("{}Override", s.ident);
let mut field_tokentrees = vec![];
let mut attrs_tokens = vec![];
for attr in &s.attrs {
if let Ok(syn::Meta::List(ml)) = attr.parse_meta() {
if ml.path.get_ident().is_some() && ml.path.get_ident().unwrap() == "cfg" {
attrs_tokens.push(attr);
}
}
}
let mut field_idents = vec![];
for f in &s.fields {
let ident = &f.ident;
@ -146,6 +154,7 @@ use super::*;
//let fields = &s.fields;
let literal_struct = quote! {
#(#attrs_tokens)*
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(deny_unknown_fields)]
pub struct #override_ident {
@ -153,6 +162,7 @@ use super::*;
}
#(#attrs_tokens)*
impl Default for #override_ident {
fn default() -> Self {
#override_ident {

348
contrib/oauth2.py 100755
View File

@ -0,0 +1,348 @@
#!/usr/bin/env python3
#
# Copyright 2012 Google Inc.
# Copyright 2020 Manos Pitsidianakis
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Performs client tasks for testing IMAP OAuth2 authentication.
To use this script, you'll need to have registered with Google as an OAuth
application and obtained an OAuth client ID and client secret.
See https://developers.google.com/identity/protocols/OAuth2 for instructions on
registering and for documentation of the APIs invoked by this code.
This script has 3 modes of operation.
1. The first mode is used to generate and authorize an OAuth2 token, the
first step in logging in via OAuth2.
oauth2 --user=xxx@gmail.com \
--client_id=1038[...].apps.googleusercontent.com \
--client_secret=VWFn8LIKAMC-MsjBMhJeOplZ \
--generate_oauth2_token
The script will converse with Google and generate an oauth request
token, then present you with a URL you should visit in your browser to
authorize the token. Once you get the verification code from the Google
website, enter it into the script to get your OAuth access token. The output
from this command will contain the access token, a refresh token, and some
metadata about the tokens. The access token can be used until it expires, and
the refresh token lasts indefinitely, so you should record these values for
reuse.
2. The script will generate new access tokens using a refresh token.
oauth2 --user=xxx@gmail.com \
--client_id=1038[...].apps.googleusercontent.com \
--client_secret=VWFn8LIKAMC-MsjBMhJeOplZ \
--refresh_token=1/Yzm6MRy4q1xi7Dx2DuWXNgT6s37OrP_DW_IoyTum4YA
3. The script will generate an OAuth2 string that can be fed
directly to IMAP or SMTP. This is triggered with the --generate_oauth2_string
option.
oauth2 --generate_oauth2_string --user=xxx@gmail.com \
--access_token=ya29.AGy[...]ezLg
The output of this mode will be a base64-encoded string. To use it, connect to a
IMAPFE and pass it as the second argument to the AUTHENTICATE command.
a AUTHENTICATE XOAUTH2 a9sha9sfs[...]9dfja929dk==
"""
import base64
import imaplib
import json
from optparse import OptionParser
import smtplib
import sys
import urllib.request, urllib.parse, urllib.error
def SetupOptionParser():
# Usage message is the module's docstring.
parser = OptionParser(usage=__doc__)
parser.add_option('--generate_oauth2_token',
action='store_true',
dest='generate_oauth2_token',
help='generates an OAuth2 token for testing')
parser.add_option('--generate_oauth2_string',
action='store_true',
dest='generate_oauth2_string',
help='generates an initial client response string for '
'OAuth2')
parser.add_option('--client_id',
default=None,
help='Client ID of the application that is authenticating. '
'See OAuth2 documentation for details.')
parser.add_option('--client_secret',
default=None,
help='Client secret of the application that is '
'authenticating. See OAuth2 documentation for '
'details.')
parser.add_option('--access_token',
default=None,
help='OAuth2 access token')
parser.add_option('--refresh_token',
default=None,
help='OAuth2 refresh token')
parser.add_option('--scope',
default='https://mail.google.com/',
help='scope for the access token. Multiple scopes can be '
'listed separated by spaces with the whole argument '
'quoted.')
parser.add_option('--test_imap_authentication',
action='store_true',
dest='test_imap_authentication',
help='attempts to authenticate to IMAP')
parser.add_option('--test_smtp_authentication',
action='store_true',
dest='test_smtp_authentication',
help='attempts to authenticate to SMTP')
parser.add_option('--user',
default=None,
help='email address of user whose account is being '
'accessed')
parser.add_option('--quiet',
action='store_true',
default=False,
dest='quiet',
help='Omit verbose descriptions and only print '
'machine-readable outputs.')
return parser
# The URL root for accessing Google Accounts.
GOOGLE_ACCOUNTS_BASE_URL = 'https://accounts.google.com'
# Hardcoded dummy redirect URI for non-web apps.
REDIRECT_URI = 'urn:ietf:wg:oauth:2.0:oob'
def AccountsUrl(command):
"""Generates the Google Accounts URL.
Args:
command: The command to execute.
Returns:
A URL for the given command.
"""
return '%s/%s' % (GOOGLE_ACCOUNTS_BASE_URL, command)
def UrlEscape(text):
# See OAUTH 5.1 for a definition of which characters need to be escaped.
return urllib.parse.quote(text, safe='~-._')
def UrlUnescape(text):
# See OAUTH 5.1 for a definition of which characters need to be escaped.
return urllib.parse.unquote(text)
def FormatUrlParams(params):
"""Formats parameters into a URL query string.
Args:
params: A key-value map.
Returns:
A URL query string version of the given parameters.
"""
param_fragments = []
for param in sorted(iter(params.items()), key=lambda x: x[0]):
param_fragments.append('%s=%s' % (param[0], UrlEscape(param[1])))
return '&'.join(param_fragments)
def GeneratePermissionUrl(client_id, scope='https://mail.google.com/'):
"""Generates the URL for authorizing access.
This uses the "OAuth2 for Installed Applications" flow described at
https://developers.google.com/accounts/docs/OAuth2InstalledApp
Args:
client_id: Client ID obtained by registering your app.
scope: scope for access token, e.g. 'https://mail.google.com'
Returns:
A URL that the user should visit in their browser.
"""
params = {}
params['client_id'] = client_id
params['redirect_uri'] = REDIRECT_URI
params['scope'] = scope
params['response_type'] = 'code'
return '%s?%s' % (AccountsUrl('o/oauth2/auth'),
FormatUrlParams(params))
def AuthorizeTokens(client_id, client_secret, authorization_code):
"""Obtains OAuth access token and refresh token.
This uses the application portion of the "OAuth2 for Installed Applications"
flow at https://developers.google.com/accounts/docs/OAuth2InstalledApp#handlingtheresponse
Args:
client_id: Client ID obtained by registering your app.
client_secret: Client secret obtained by registering your app.
authorization_code: code generated by Google Accounts after user grants
permission.
Returns:
The decoded response from the Google Accounts server, as a dict. Expected
fields include 'access_token', 'expires_in', and 'refresh_token'.
"""
params = {}
params['client_id'] = client_id
params['client_secret'] = client_secret
params['code'] = authorization_code
params['redirect_uri'] = REDIRECT_URI
params['grant_type'] = 'authorization_code'
request_url = AccountsUrl('o/oauth2/token')
response = urllib.request.urlopen(request_url, urllib.parse.urlencode(params).encode()).read()
return json.loads(response)
def RefreshToken(client_id, client_secret, refresh_token):
"""Obtains a new token given a refresh token.
See https://developers.google.com/accounts/docs/OAuth2InstalledApp#refresh
Args:
client_id: Client ID obtained by registering your app.
client_secret: Client secret obtained by registering your app.
refresh_token: A previously-obtained refresh token.
Returns:
The decoded response from the Google Accounts server, as a dict. Expected
fields include 'access_token', 'expires_in', and 'refresh_token'.
"""
params = {}
params['client_id'] = client_id
params['client_secret'] = client_secret
params['refresh_token'] = refresh_token
params['grant_type'] = 'refresh_token'
request_url = AccountsUrl('o/oauth2/token')
response = urllib.request.urlopen(request_url, urllib.parse.urlencode(params).encode()).read()
return json.loads(response)
def GenerateOAuth2String(username, access_token, base64_encode=True):
"""Generates an IMAP OAuth2 authentication string.
See https://developers.google.com/google-apps/gmail/oauth2_overview
Args:
username: the username (email address) of the account to authenticate
access_token: An OAuth2 access token.
base64_encode: Whether to base64-encode the output.
Returns:
The SASL argument for the OAuth2 mechanism.
"""
auth_string = 'user=%s\1auth=Bearer %s\1\1' % (username, access_token)
if base64_encode:
auth_string = base64.b64encode(bytes(auth_string, 'utf-8'))
return auth_string
def TestImapAuthentication(user, auth_string):
"""Authenticates to IMAP with the given auth_string.
Prints a debug trace of the attempted IMAP connection.
Args:
user: The Gmail username (full email address)
auth_string: A valid OAuth2 string, as returned by GenerateOAuth2String.
Must not be base64-encoded, since imaplib does its own base64-encoding.
"""
print()
imap_conn = imaplib.IMAP4_SSL('imap.gmail.com')
imap_conn.debug = 4
imap_conn.authenticate('XOAUTH2', lambda x: auth_string)
imap_conn.select('INBOX')
def TestSmtpAuthentication(user, auth_string):
"""Authenticates to SMTP with the given auth_string.
Args:
user: The Gmail username (full email address)
auth_string: A valid OAuth2 string, not base64-encoded, as returned by
GenerateOAuth2String.
"""
print()
smtp_conn = smtplib.SMTP('smtp.gmail.com', 587)
smtp_conn.set_debuglevel(True)
smtp_conn.ehlo('test')
smtp_conn.starttls()
smtp_conn.docmd('AUTH', 'XOAUTH2 ' + base64.b64encode(auth_string))
def RequireOptions(options, *args):
missing = [arg for arg in args if getattr(options, arg) is None]
if missing:
print('Missing options: %s' % ' '.join(missing), file=sys.stderr)
sys.exit(-1)
def main(argv):
options_parser = SetupOptionParser()
(options, args) = options_parser.parse_args()
if options.refresh_token:
RequireOptions(options, 'client_id', 'client_secret')
response = RefreshToken(options.client_id, options.client_secret,
options.refresh_token)
if options.quiet:
print(response['access_token'])
else:
print('Access Token: %s' % response['access_token'])
print('Access Token Expiration Seconds: %s' % response['expires_in'])
elif options.generate_oauth2_string:
RequireOptions(options, 'user', 'access_token')
oauth2_string = GenerateOAuth2String(options.user, options.access_token)
if options.quiet:
print(oauth2_string.decode('utf-8'))
else:
print('OAuth2 argument:\n' + oauth2_string.decode('utf-8'))
elif options.generate_oauth2_token:
RequireOptions(options, 'client_id', 'client_secret')
print('To authorize token, visit this url and follow the directions:')
print(' %s' % GeneratePermissionUrl(options.client_id, options.scope))
authorization_code = input('Enter verification code: ')
response = AuthorizeTokens(options.client_id, options.client_secret,
authorization_code)
print('Refresh Token: %s' % response['refresh_token'])
print('Access Token: %s' % response['access_token'])
print('Access Token Expiration Seconds: %s' % response['expires_in'])
elif options.test_imap_authentication:
RequireOptions(options, 'user', 'access_token')
TestImapAuthentication(options.user,
GenerateOAuth2String(options.user, options.access_token,
base64_encode=False))
elif options.test_smtp_authentication:
RequireOptions(options, 'user', 'access_token')
TestSmtpAuthentication(options.user,
GenerateOAuth2String(options.user, options.access_token,
base64_encode=False))
else:
options_parser.print_help()
print('Nothing to do, exiting.')
return
if __name__ == '__main__':
main(sys.argv)

22
debian/changelog vendored
View File

@ -1,3 +1,25 @@
meli (0.6.2-1) buster; urgency=low
Added
- Add customizable mailbox tree in sidebar
- Make `dbus` dependency opt-out (feature is `dbus-notifications`)
- Implemented JMAP async, search, tagging, syncing
- Preserve account order from configuration file
- Implemented IMAP `CONDSTORE` support for IMAP cache
- Add `timeout` setting for IMAP
- Implement TCP keepalive for IMAP
- Rewrote email address parsers.
- Implement `copy_messages` for maildir
- Implement selection with motions
Fixed
- Fixed various problems with IMAP cache
- Fixed various problems with IMAP message counts
- Fixed various problems with IMAP connection hanging
- Fixed IMAP not reconnecting on dropped IDLE connections
- Fixed various problems with notmuch backend
-- Manos Pitsidianakis <epilys@nessuent.xyz> Thu, 24 Sep 2020 18:14:00 +0200
meli (0.6.1-1) buster; urgency=low
* added experimental NNTP backend

6
debian/meli.docs vendored
View File

@ -1,3 +1,3 @@
meli.1
meli.conf.5
meli-themes.5
docs/meli.1
docs/meli.conf.5
docs/meli-themes.5

View File

@ -193,6 +193,8 @@ widgets.options.highlighted
.It
mail.sidebar
.It
mail.sidebar_divider
.It
mail.sidebar_unread_count
.It
mail.sidebar_index
@ -261,6 +263,10 @@ mail.listing.conversations.selected
.It
mail.view.headers
.It
mail.view.headers_names
.It
mail.view.headers_area
.It
mail.view.body
.It
mail.view.thread.indentation.a
@ -600,7 +606,7 @@ Yellow6:148:_:Grey93:255
.Xr meli 1 ,
.Xr meli.conf 5
.Sh CONFORMING TO
TOML Standard v.0.5.0 https://github.com/toml-lang/toml/blob/master/versions/en/toml-v0.5.0.md
TOML Standard v.0.5.0 https://toml.io/en/v0.5.0
.sp
https://no-color.org/
.Sh AUTHORS

View File

@ -171,7 +171,7 @@ To search in specific fields, prepend your search keyword with "field:" like so:
.Pp
.D1 not ((from:unrealistic and (to:complex or not "query")) or flags:seen,draft)
.Pp
.D1 alladdresses:mailing@list.tld and cc:me@domain.tld
.D1 alladdresses:mailing@example.com and cc:me@example.com
.Pp
Boolean operators are
.Em or Ns
@ -360,7 +360,9 @@ is the default mode
.It COMMAND
commands are issued in
.Em COMMAND
mode, by default started with Space and exited with
mode, by default started with
.Cm \&:
and exited with
.Cm Esc
key.
.It EMBED
@ -395,15 +397,29 @@ where
is a mailbox prefixed with the
.Ar n
number in the side menu for the current account
.It Cm toggle_thread_snooze
.It Cm toggle thread_snooze
don't issue notifications for thread under cursor in thread listing
.It Cm search Ar STRING
search mailbox with
.Ar STRING
key.
Escape exits search results
.It Cm set read, set unread
Set read status of message.
query.
Escape exits search results.
.It Cm select Ar STRING
select threads matching
.Ar STRING
query.
.It Cm set seen, set unseen
Set seen status of message.
.It Cm import Ar FILEPATH Ar MAILBOX_PATH
Import mail from file into given mailbox.
.It Cm copyto, moveto Ar MAILBOX_PATH
Copy or move to other mailbox.
.It Cm copyto, moveto Ar ACCOUNT Ar MAILBOX_PATH
Copy or move to another account's mailbox.
.It Cm delete
Delete selected threads.
.It Cm export-mbox Ar FILEPATH
Export selected threads to mboxcl2 file.
.It Cm create-mailbox Ar ACCOUNT Ar MAILBOX_PATH
create mailbox with given path.
Be careful with backends and separator sensitivity (eg IMAP)
@ -439,6 +455,16 @@ as an attachment
in composer, pipe
.Ar CMD Ar ARGS
output into an attachment
.It Cm add-attachment-file-picker
Launch command defined in the configuration value
.Ic file_picker_command
in
.Xr meli.conf 5 TERMINAL
.It Cm add-attachment-file-picker < Ar CMD Ar ARGS
Launch command
.Ar CMD Ar ARGS Ns
\&.
The command should print file paths in stderr, separated by NULL bytes.
.It Cm remove-attachment Ar INDEX
remove attachment with given index
.It Cm toggle sign
@ -464,6 +490,15 @@ to
.It Cm printenv Ar KEY
print environment variable
.Ar KEY
.It Cm quit
Quits
.Nm Ns
\&.
.It Cm reload-config
Reloads configuration but only if account configuration is unchanged.
Useful if you want to reload some settings without restarting
.Nm Ns
\&.
.El
.Sh SHORTCUTS
See

View File

@ -78,7 +78,7 @@ example configuration
root_mailbox = "/path/to/root/folder"
format = "Maildir"
index_style = "Compact"
identity="email@address.tld"
identity="email@example.com"
subscribed_mailboxes = ["folder", "folder/Sent"] # or [ "*", ] for all mailboxes
display_name = "Name"
@ -107,7 +107,7 @@ script = "notify-send"
[composing]
# required for sending e-mail
send_mail = 'msmtp --read-recipients --read-envelope-from'
#send_mail = { hostname = "smtp.mail.tld", port = 587, auth = { type = "auto", username = "user", password = { type = "command_eval", value = "gpg2 --no-tty -q -d ~/.passwords/user.gpg" } }, security = { type = "STARTTLS" } }
#send_mail = { hostname = "smtp.example.com", port = 587, auth = { type = "auto", username = "user", password = { type = "command_eval", value = "gpg2 --no-tty -q -d ~/.passwords/user.gpg" } }, security = { type = "STARTTLS" } }
editor_command = 'vim +/^$'
[shortcuts]
@ -152,7 +152,7 @@ plain:shows one row per mail, regardless of threading
.Bl -tag -width 36n
.It Ic display_name Ar String
.Pq Em optional
A name which can be combined with your address: "Name <email@address.tld>".
A name which can be combined with your address: "Name <email@example.com>".
.It Ic read_only Ar boolean
Attempt to not make any changes to this account.
.Pq Em false
@ -199,14 +199,14 @@ format = "notmuch"
[accounts.notmuch.mailboxes]
"INBOX" = { query="tag:inbox", subscribe = true }
"Drafts" = { query="tag:draft", subscribe = true }
"Sent" = { query="from:username@server.tld from:username2@server.tld", subscribe = true }
"Sent" = { query="from:username@example.com from:username2@example.com", subscribe = true }
.Ed
.Ss IMAP only
IMAP specific options are:
.Bl -tag -width 36n
.It Ic server_hostname Ar String
example:
.Qq mail.example.tld
.Qq mail.example.com
.It Ic server_username Ar String
Server username
.It Ic server_password Ar String
@ -235,11 +235,25 @@ Do not validate TLS certificates.
Use IDLE extension.
.\" default value
.Pq Em true
.It Ic use_condstore Ar boolean
.Pq Em optional
Use CONDSTORE extension.
.\" default value
.Pq Em true
.It Ic use_deflate Ar boolean
.Pq Em optional
Use COMPRESS=DEFLATE extension (if built with DEFLATE support).
.\" default value
.Pq Em true
.It Ic use_oauth2 Ar boolean
.Pq Em optional
Use OAUTH2 authentication.
Can only be used with
.Ic server_password_command
which should return a base64-encoded OAUTH2 token ready to be passed to IMAP.
For help on setup with Gmail, see Gmail section below.
.\" default value
.Pq Em false
.It Ic timeout Ar integer
.Pq Em optional
Timeout to use for server connections in seconds.
@ -247,12 +261,41 @@ A timeout of 0 seconds means there's no timeout.
.\" default value
.Pq Em 16
.El
.Ss Gmail
Gmail has non-standard IMAP behaviors that need to be worked around.
.Ss Gmail - sending mail
Option
.Ic store_sent_mail
should be disabled since Gmail auto-saves sent mail by its own.
.Ss Gmail OAUTH2
To use OAUTH2, you must go through a process to register your own private "application" with Google that can use OAUTH2 tokens.
For convenience in the meli repository under the
.Pa contrib/
directory you can find a python3 file named oauth2.py to generate and request the appropriate data to perform OAUTH2 authentication.
Steps:
.Bl -bullet -compact
.It
In Google APIs, create a custom OAuth client ID and note down the Client ID and Client Secret.
You may need to create a consent screen; follow the steps described in the website.
.It
Run the oauth2.py script as follows (after adjusting binary paths and credentials):
.Cm python3 oauth2.py --user=xxx@gmail.com --client_id=1038[...].apps.googleusercontent.com --client_secret=VWFn8LIKAMC-MsjBMhJeOplZ --generate_oauth2_token
and follow the instructions.
Note down the refresh token.
.It
In
.Ic server_password_command
enter a command like this (after adjusting binary paths and credentials):
.Cm TOKEN=$(python3 oauth2.py --user=xxx@gmail.com --quiet --client_id=1038[...].apps.googleusercontent.com --client_secret=VWFn8LIKAMC-MsjBMhJeOplZ --refresh_token=1/Yzm6MRy4q1xi7Dx2DuWXNgT6s37OrP_DW_IoyTum4YA) && python3 oauth2.py --user=xxx@gmail.com --generate_oauth2_string --quiet --access_token=$TOKEN
.It
On startup, meli should evaluate this command which if successful must only return a base64-encoded token ready to be passed to IMAP.
.El
.Ss JMAP only
JMAP specific options
.Bl -tag -width 36n
.It Ic server_hostname Ar String
example:
.Qq mail.example.tld
.Qq mail.example.com
.It Ic server_username Ar String
Server username
.It Ic server_password Ar String
@ -360,9 +403,9 @@ and
\&.
Example:
.Bd -literal
[accounts."imap.domain.tld".mailboxes."INBOX"]
[accounts."imap.example.com".mailboxes."INBOX"]
index_style = "plain"
[accounts."imap.domain.tld".mailboxes."INBOX".pager]
[accounts."imap.example.com".mailboxes."INBOX".pager]
filter = ""
.Ed
.El
@ -397,6 +440,37 @@ Add meli User-Agent header in new drafts
.Pq Em true
.It Ic default_header_values Ar hash table String[String]
Default header values used when creating a new draft.
.It Ic store_sent_mail Ar boolean
.Pq Em optional
Store sent mail after successful submission.
This setting is meant to be disabled for non-standard behaviour in gmail, which auto-saves sent mail on its own.
.\" default value
.Pq Em true
.It Ic attribution_format_string Ar String
.Pq Em optional
The attribution line appears above the quoted reply text.
The format specifiers for the replied address are:
.Bl -bullet -compact
.It
.Li %+f
— the sender's name and email address.
.It
.Li %+n
— the sender's name (or email address, if no name is included).
.It
.Li %+a
— the sender's email address.
.El
The format string is passed to
.Xr strftime 3
with the replied envelope's date.
.\" default value
.Pq Em "On %a, %0e %b %Y %H:%M, %+f wrote:%n"
.It Ic attribution_use_posix_locale Ar boolean
.Pq Em optional
Whether the strftime call for the attribution string uses the POSIX locale instead of the user's active locale.
.\" default value
.Pq Em true
.El
.Sh SHORTCUTS
Shortcuts can take the following values:
@ -466,12 +540,16 @@ exit_thread = 'i'
Toggle help and shortcuts view.
.\" default value
.Pq Em \&?
.It Ic quit
Quit application.
.\" default value
.Pq Ql Em q
.It Ic enter_command_mode
Enter
.Em COMMAND
mode.
.\" default value
.Pq Ql Em \
.Pq Ql Em \&:
.It Ic next_tab
Go to next tab.
.\" default value
@ -787,19 +865,27 @@ Sets the string to print in the mailbox tree for a leaf level where its root has
.It Ic sidebar_mailbox_tree_no_sibling_leaf Ar String
.Pq Em optional
Sets the string to print in the mailbox tree for a leaf level where its root has no sibling.
.It Ic sidebar_divider Ar char
.Pq Em optional
Sets the character to print as the divider between the accounts list and the message list.
.It Ic show_menu_scrollbar Ar boolean
.Pq Em optional
Show auto-hiding scrollbar in accounts sidebar menu.
.\" default value
.Pq Em true
.El
.Ss Examples of sidebar mailbox tree customization
The default values
.Bd
.sp
.Bd -literal
has_sibling = " "
no_sibling = " ";
has_sibling_leaf = " "
no_sibling_leaf = " "
.Ed
.sp
render a mailbox tree like the following:
.sp
.Bd -literal
0 Inbox 3
1 Archive
@ -807,20 +893,20 @@ render a mailbox tree like the following:
3 Lists
4 example-list-a
5 example-list-b
6 Sent
6 Sent
7 Spam
8 Trash
.Ed
.sp
Other possible trees:
.sp
.Bd -literal
has_sibling = " ┃"
no_sibling = " "
has_sibling_leaf = " ┣━"
no_sibling_leaf = " ┗━"
.Ed
.sp
.Bd -literal
0 Inbox 3
1 ┣━Archive
@ -828,20 +914,20 @@ no_sibling_leaf = " ┗━"
3 ┣━Lists
4 ┃ ┣━example-list-a
5 ┃ ┗━example-list-b
6 ┣━Sent
6 ┣━Sent
7 ┣━Spam
8 ┗━Trash
.Ed
.sp
A completely ASCII one:
.sp
.Bd -literal
has_sibling = " |"
no_sibling = " "
has_sibling_leaf = " |\\_"
no_sibling_leaf = " \\_"
.Ed
.sp
.Bd -literal
0 Inbox 3
1 |\\_Archive
@ -849,11 +935,11 @@ no_sibling_leaf = " \\_"
3 |\\_Lists
4 | |\\_example-list-a
5 | \\_example-list-b
6 |\\_Sent
6 |\\_Sent
7 |\\_Spam
8 \\_Trash
.Ed
.sp
.Sh TAGS
.Bl -tag -width 36n
.It Ic colours Ar hash table String[Color]
@ -892,11 +978,6 @@ Always sign sent messages
Key to be used when signing/encrypting (not functional yet)
.\" default value
.Pq Em none
.It Ic gpg_binary Ar String
.Pq Em optional
The gpg binary name or file location to use
.\" default value
.Pq Em "gpg2"
.El
.Sh TERMINAL
.Bl -tag -width 36n
@ -920,6 +1001,14 @@ If false, no ANSI colors are used.
Set window title in xterm compatible terminals An empty string means no window title is set.
.\" default value
.Pq Em "meli"
.It Ic file_picker_command Ar String
.Pq Em optional
Set command that prints file paths in stderr, separated by NULL bytes.
Used with
.Ic add-attachment-file-picker
when composing new mail.
.\" default value
.Pq Em None
.It Ic themes Ar hash table String[String[Attribute]]
Define UI themes.
See
@ -938,6 +1027,80 @@ theme = "themeB"
[terminal.themes.themeC]
\&...
.Ed
.It Ic use_mouse Ar bool
Use mouse events.
This will disable text selection, but you will be able to resize some widgets.
This setting can be toggled with
.Cm toggle mouse Ns
\&.
.\" default value
.Pq Em false
.It Ic mouse_flag Ar String
String to show in status bar if mouse is active.
.\" default value
.Pq Em 🖱️
.It Ic progress_spinner_sequence Ar Either \&< Integer, ProgressSpinner \&>
Choose between 37 built in sequences (integers between 0-36) or define your own list of strings for the progress spinner animation.
Set to an empty array to disable the progress spinner.
.\" default value
.Pq Em 20
.Pp
Builtin sequences are:
.Bd -literal
0 ["-", "\\", "|", "/"]
1 ["▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"]
2 ["⣀", "⣄", "⣤", "⣦", "⣶", "⣷", "⣿"]
3 ["⣀", "⣄", "⣆", "⣇", "⣧", "⣷", "⣿"]
4 ["○", "◔", "◐", "◕", "⬤"]
5 ["□", "◱", "◧", "▣", "■"]
6 ["□", "◱", "▨", "▩", "■"]
7 ["□", "◱", "▥", "▦", "■"]
8 ["░", "▒", "▓", "█"]
9 ["░", "█"]
10 ["⬜", "⬛"]
11 ["▱", "▰"]
12 ["▭", "◼"]
13 ["▯", "▮"]
14 ["◯", "⬤"]
15 ["⚪", "⚫"]
16 ["▖", "▗", "▘", "▝", "▞", "▚", "▙", "▟", "▜", "▛"]
17 ["|", "/", "-", "\\"]
18 [".", "o", "O", "@", "*"]
19 ["◡◡", "⊙⊙", "◠◠", "⊙⊙"]
20 ["◜ ", " ◝", " ◞", "◟ "]
21 ["←", "↖", "↑", "↗", "→", "↘", "↓", "↙"]
22 ["▁", "▃", "▄", "▅", "▆", "▇", "█", "▇", "▆", "▅", "▄", "▃"]
23 [ "▉", "▊", "▋", "▌", "▍", "▎", "▏", "▎", "▍", "▌", "▋", "▊", "▉" ]
24 ["▖", "▘", "▝", "▗"]
25 ["▌", "▀", "▐", "▄"]
26 ["┤", "┘", "┴", "└", "├", "┌", "┬", "┐"]
27 ["◢", "◣", "◤", "◥"]
28 ["⠁", "⠂", "⠄", "⡀", "⢀", "⠠", "⠐", "⠈"]
29 ["⢎⡰", "⢎⡡", "⢎⡑", "⢎⠱", "⠎⡱", "⢊⡱", "⢌⡱", "⢆⡱"]
30 [".", "o", "O", "°", "O", "o", "."]
31 ["㊂", "㊀", "㊁"]
32 ["💛 ", "💙 ", "💜 ", "💚 ", "❤️ "]
33 [ "🕛 ", "🕐 ", "🕑 ", "🕒 ", "🕓 ", "🕔 ", "🕕 ", "🕖 ", "🕗 ", "🕘 ", "🕙 ", "🕚 " ]
34 ["🌍 ", "🌎 ", "🌏 "]
35 [ "[ ]", "[= ]", "[== ]", "[=== ]", "[ ===]", "[ ==]", "[ =]", "[ ]", "[ =]", "[ ==]", "[ ===]", "[====]", "[=== ]", "[== ]", "[= ]" ]
36 ["🌑 ", "🌒 ", "🌓 ", "🌔 ", "🌕 ", "🌖 ", "🌗 ", "🌘 "]
.Ed
.Pp
Or, define an array of strings each consisting of a frame in the progress sequence indicator for a custom spinner:
.Bl -tag -width 36n
.It Ic interval_ms Ar u64
.Pq Em optional
Frame interval.
.\" default value
.Pq 50
.It Ic frames Ar [String]
The animation frames.
.El
.Pp
Example:
.Bd -literal
progress_spinner_sequence = { interval_ms = 150, frames = [ "-", "=", "≡" ] }
.Ed
.El
.Sh LOG
.Bl -tag -width 36n
@ -1012,25 +1175,62 @@ subsection
.El
.Ss SmtpAuth
.Bl -tag -width 36n
.It Ic type Ar "none" | "auto"
.It Ic type Ar "none" | "auto" | "xoauth2"
.El
.Pp
For type "auto":
.Bl -tag -width 36n
.It Ic username Ar String
.It Ic password Ar String|SmtpPassword
.It Ic password Ar SmtpPassword
.It Ic require_auth Ar bool
.Pq Em optional
require authentication in every case
.\" default value
.Pq Em true
.El
.sp
For type "xoauth2":
.Bl -tag -width 36n
.It Ic token_command Ar String
Command to evaluate that returns an XOAUTH2 token.
.It Ic require_auth Ar bool
.Pq Em optional
require authentication in every case
.\" default value
.Pq Em true
.El
.sp
Examples:
.Bd -literal
auth = { type = "auto", username = "user", password = { type = "raw", value = "hunter2" } }
.Ed
.Bd -literal
auth = { type = "auto", username = "user", password = "hunter2" }
.Ed
.Bd -literal
auth = { type = "none" }
.Ed
.sp
For Gmail (see
.Sx Gmail OAUTH2
for details on the authentication token command):
.Bd -literal
auth = { type = "xoauth2", token_command = "TOKEN=$(python3 oauth2.py --user=xxx@gmail.com --quiet --client_id=1038[...].apps.googleusercontent.com --client_secret=[..] --refresh_token=[..] && python3 oauth2.py --user=xxx@gmail.com --generate_oauth2_string --quiet --access_token=$TOKEN" }
.Ed
.Ss SmtpPassword
.Bl -tag -width 36n
.It Ic type Ar "raw" | "command_evaluation"
.It Ic value Ar String
Either a raw password string, or command to execute.
.El
.sp
Examples:
.Bd -literal
password = { type = "raw", value = "hunter2" }
.Ed
.Bd -literal
password = { type = "command_eval", value = "gpg2 --no-tty -q -d ~/.passwords/user.gpg" }
.Ed
.Ss SmtpSecurity
Default security type is
.Em auto Ns

View File

@ -12,7 +12,7 @@
#root_mailbox = "/path/to/root/mailbox"
#format = "Maildir"
#index_style = "Conversations" # or [plain, threaded, compact]
#identity="email@address.tld"
#identity="email@example.com"
#display_name = "Name"
#subscribed_mailboxes = ["INBOX", "INBOX/Sent", "INBOX/Drafts", "INBOX/Junk"]
#
@ -33,14 +33,14 @@
#[accounts."imap"]
#root_mailbox = "INBOX"
#format = "imap"
#server_hostname="mail.server.tld"
#server_hostname="mail.example.com"
#server_password="pha2hiLohs2eeeish2phaii1We3ood4chakaiv0hien2ahie3m"
#server_username="username@server.tld"
#server_username="username@example.com"
#server_port="993" # imaps
#server_port="143" # STARTTLS
#use_starttls=true #optional
#index_style = "Conversations"
#identity = "username@server.tld"
#identity = "username@example.com"
#display_name = "Name Name"
### match every mailbox:
#subscribed_mailboxes = ["*" ]
@ -52,14 +52,32 @@
#root_mailbox = "/path/to/folder" # where .notmuch/ directory is located
#format = "notmuch"
#index_style = "conversations"
#identity="username@server.tld"
#identity="username@example.com"
#display_name = "Name Name"
# # notmuch mailboxes are virtual, they are defined by their alias and the notmuch query that corresponds to their content.
# [accounts.notmuch.mailboxes]
# "INBOX" = { query="tag:inbox", subscribe = true }
# "Drafts" = { query="tag:draft", subscribe = true }
# "Sent" = { query="from:username@server.tld from:username2@server.tld", subscribe = true }
##
# "Sent" = { query="from:username@example.com from:username2@example.com", subscribe = true }
#
## Setting up a Gmail account
#[accounts."gmail"]
#root_mailbox = '[Gmail]'
#format = "imap"
#server_hostname='imap.gmail.com'
#server_password="password"
#server_username="username@gmail.com"
#server_port="993"
#index_style = "Conversations"
#identity = "username@gmail.com"
#display_name = "Name Name"
### match every mailbox:
#subscribed_mailboxes = ["*" ]
#composing.send_mail = 'msmtp --read-recipients --read-envelope-from'
### Gmail auto saves sent mail to Sent folder, so don't duplicate the effort:
#composing.store_sent_mail = false
#
#
#[pager]
#filter = "COLUMNS=72 /usr/local/bin/pygmentize -l email"
#pager_context = 0 # default, optional
@ -105,14 +123,13 @@
#[composing]
##required for sending e-mail
#send_mail = 'msmtp --read-recipients --read-envelope-from'
##send_mail = { hostname = "smtp.mail.tld", port = 587, auth = { type = "auto", username = "user", password = { type = "command_eval", value = "gpg2 --no-tty -q -d ~/.passwords/user.gpg" } }, security = { type = "STARTTLS" } }
##send_mail = { hostname = "smtp.example.com", port = 587, auth = { type = "auto", username = "user", password = { type = "command_eval", value = "gpg2 --no-tty -q -d ~/.passwords/user.gpg" } }, security = { type = "STARTTLS" } }
#editor_command = 'vim +/^$' # optional, by default $EDITOR is used.
#
#
#[pgp]
#auto_sign = false # always sign sent messages
#auto_verify_signatures = true # always verify signatures when reading signed e-mails
#gpg_binary = "/usr/bin/gpg2" #optional
#
#[terminal]
#theme = "dark" # or "light"

View File

@ -0,0 +1,70 @@
[terminal.themes.nord]
"theme_default" = { fg = "$nord6", bg = "$nord0", attrs = "Default" }
"mail.listing.compact.even" = { fg = "theme_default", bg = "$nord1", attrs = "theme_default" }
"mail.listing.compact.odd" = { fg = "theme_default", bg = "$nord2", attrs = "theme_default" }
"mail.listing.plain.even" = { fg = "theme_default", bg = "$nord1", attrs = "theme_default" }
"mail.listing.plain.odd" = { fg = "theme_default", bg = "$nord2", attrs = "theme_default" }
"mail.listing.compact.even_highlighted" = { fg = "$nord0", bg = "$focused_bg", attrs = "theme_default" }
"mail.listing.compact.odd_highlighted" = { fg = "$nord0", bg = "$focused_bg", attrs = "theme_default" }
"mail.listing.conversations.highlighted" = { fg = "$nord0", bg = "$focused_bg", attrs = "theme_default" }
"mail.listing.conversations.selected" = { fg = "theme_default", bg = "LightCoral", attrs = "theme_default" }
"mail.listing.conversations.subject" = { fg = "theme_default", bg = "theme_default", attrs = "theme_default" }
"mail.listing.conversations.unseen" = { fg = "theme_default", bg = "$nord2", attrs = "theme_default" }
"mail.listing.plain.even_highlighted" = { fg = "$nord0", bg = "$focused_bg", attrs = "theme_default" }
"mail.listing.plain.odd_highlighted" = { fg = "$nord0", bg = "$focused_bg", attrs = "theme_default" }
"mail.listing.tag_default" = { fg = "theme_default", bg = "$nord8", attrs = "theme_default" }
"mail.sidebar_highlighted" = { fg = "$nord1", bg = "$focused_bg", attrs = "theme_default" }
"mail.sidebar_highlighted_account" = { fg = "$nord5", bg = "$nord1", attrs = "theme_default" }
"mail.sidebar_highlighted_account_name" = { fg = "mail.sidebar_highlighted_account", bg = "mail.sidebar_highlighted_account", attrs = "theme_default" }
"mail.sidebar_highlighted_account_index" = { fg = "mail.sidebar_highlighted_account", bg = "mail.sidebar_highlighted_account", attrs = "theme_default" }
"mail.sidebar_highlighted_account_unread_count" = { fg = "mail.sidebar_highlighted_account", bg = "mail.sidebar_highlighted_account", attrs = "theme_default" }
"mail.sidebar_highlighted_index" = { fg = "mail.sidebar_index", bg = "mail.sidebar_highlighted", attrs = "theme_default" }
"mail.sidebar_highlighted_unread_count" = { fg = "mail.sidebar_highlighted", bg = "mail.sidebar_highlighted", attrs = "theme_default" }
"mail.sidebar" = { fg = "$nord5", bg = "theme_default", attrs = "theme_default" }
"mail.sidebar_account_name" = { fg = "$nord5", bg = "theme_default", attrs = "theme_default" }
"mail.sidebar_index" = { fg = "$nord1", bg = "theme_default", attrs = "theme_default" }
"mail.sidebar_unread_count" = { fg = "$nord1", bg = "theme_default", attrs = "theme_default" }
"mail.view.body" = { fg = "theme_default", bg = "theme_default", attrs = "theme_default" }
"mail.view.headers" = { fg = "$nord9", bg = "theme_default", attrs = "theme_default" }
"mail.view.thread.indentation.a" = { fg = "theme_default", bg = "$nord11", attrs = "theme_default" }
"mail.view.thread.indentation.b" = { fg = "theme_default", bg = "$nord12", attrs = "theme_default" }
"mail.view.thread.indentation.c" = { fg = "theme_default", bg = "$nord13", attrs = "theme_default" }
"mail.view.thread.indentation.d" = { fg = "theme_default", bg = "$nord14", attrs = "theme_default" }
"mail.view.thread.indentation.e" = { fg = "theme_default", bg = "$nord15", attrs = "theme_default" }
"mail.view.thread.indentation.f" = { fg = "theme_default", bg = "$nord13", attrs = "theme_default" }
"pager.highlight_search" = { fg = "$nord5", bg = "$nord7", attrs = "Bold" }
"pager.highlight_search_current" = { fg = "$nord7", bg = "$nord10", attrs = "Bold" }
"status.bar" = { fg = "$nord5", bg = "$nord3", attrs = "theme_default" }
"status.notification" = { fg = "$nord5", bg = "$nord3", attrs = "theme_default" }
"tab.bar" = { fg = "theme_default", bg = "theme_default", attrs = "theme_default" }
"tab.focused" = { fg = "$nord1", bg = "$focused_bg", attrs = "theme_default" }
"tab.unfocused" = { fg = "$nord4", bg = "$unfocused_bg", attrs = "theme_default" }
"widgets.form.field" = { fg = "theme_default", bg = "theme_default", attrs = "theme_default" }
"widgets.form.highlighted" = { fg = "theme_default", bg = "$nord2", attrs = "theme_default" }
"widgets.form.label" = { fg = "theme_default", bg = "theme_default", attrs = "Bold" }
"widgets.list.header" = { fg = "theme_default", bg = "theme_default", attrs = "Bold" }
"widgets.options.highlighted" = { fg = "theme_default", bg = "$nord2", attrs = "theme_default" }
[terminal.themes.nord.color_aliases]
nord0 = "#2e3440"
nord1 = "#3b4252"
nord2 = "#434c5e"
nord3 = "#4c566a"
# snow storm
nord4 = "#d8dee9"
nord5 = "#e5e9f0"
nord6 = "#eceff4"
# frost
nord7 = "#8fbcbb"
nord8 = "#88c0d0"
nord9 = "#81a1c1"
nord10 = "#5e81ac"
# aurora
nord11 = "#bf616a"
nord12 = "#d08770"
nord13 = "#ebcb8b"
nord14 = "#a3be8c"
nord15 = "#b48ead"
# semantics
focused_bg = "$nord8"
unfocused_bg = "$nord3"

View File

@ -51,7 +51,7 @@ color_aliases = { "neon_green" = "#6ef9d4", "darkgrey" = "#4a4a4a", "neon_purp
"pager.highlight_search" = { fg = "White", bg = "Teal", attrs = "Bold" }
"pager.highlight_search_current" = { fg = "White", bg = "NavyBlue", attrs = "Bold" }
"status.bar" = { fg = "White", bg = "Black", attrs = "theme_default" }
"status.notification" = { fg = "Plum1", bg = "DarkRed", attrs = "theme_default" }
"status.notification" = { fg = "Plum1", bg = "theme_default", attrs = "theme_default" }
"tab.bar" = { fg = "theme_default", bg = "theme_default", attrs = "theme_default" }
"tab.focused" = { fg = "theme_default", bg = "theme_default", attrs = "theme_default" }
"tab.unfocused" = { fg = "$darkgrey", bg = "Black", attrs = "theme_default" }

View File

@ -0,0 +1,69 @@
[terminal.themes.sail]
color_aliases = { "unseen_fg" = "theme_default", "unseen_bg" = "theme_default", "sea" = "#91C7FF", "dimmed_text" = "#afbec5", "dimmed_bg" = "Grey78", "header" = "#edeff1" }
"theme_default" = { fg = "#37474f", bg = "White", attrs = "Default" }
"mail.listing.attachment_flag" = { fg = "Blue", bg = "theme_default", attrs = "theme_default" }
"mail.listing.compact.even" = { fg = "$dimmed_text", bg = "theme_default", attrs = "theme_default" }
"mail.listing.compact.even_highlighted" = { fg = "theme_default", bg = "$dimmed_bg", attrs = "theme_default" }
"mail.listing.compact.even_selected" = { fg = "$dimmed_text", bg = "LightCoral", attrs = "theme_default" }
"mail.listing.compact.even_unseen" = { fg = "$unseen_fg", bg = "$unseen_bg", attrs = "theme_default" }
"mail.listing.compact.odd" = { fg = "$dimmed_text", bg = "theme_default", attrs = "theme_default" }
"mail.listing.compact.odd_highlighted" = { fg = "theme_default", bg = "$dimmed_bg", attrs = "theme_default" }
"mail.listing.compact.odd_selected" = { fg = "$dimmed_text", bg = "LightCoral", attrs = "theme_default" }
"mail.listing.compact.odd_unseen" = { fg = "$unseen_fg", bg = "$unseen_bg", attrs = "theme_default" }
"mail.listing.plain.even" = { fg = "mail.listing.compact.even", bg = "mail.listing.compact.even", attrs = "theme_default" }
"mail.listing.plain.even_highlighted" = { fg = "mail.listing.compact.even_highlighted", bg = "mail.listing.compact.even_highlighted", attrs = "theme_default" }
"mail.listing.plain.even_selected" = { fg = "mail.listing.compact.even_selected", bg = "mail.listing.compact.even_selected", attrs = "theme_default" }
"mail.listing.plain.even_unseen" = { fg = "mail.listing.compact.even_unseen", bg = "mail.listing.compact.even_unseen", attrs = "theme_default" }
"mail.listing.plain.odd" = { fg = "mail.listing.compact.odd", bg = "mail.listing.compact.odd", attrs = "theme_default" }
"mail.listing.plain.odd_highlighted" = { fg = "mail.listing.compact.odd_highlighted", bg = "mail.listing.compact.odd_highlighted", attrs = "theme_default" }
"mail.listing.plain.odd_selected" = { fg = "mail.listing.compact.odd_selected", bg = "mail.listing.compact.odd_selected", attrs = "theme_default" }
"mail.listing.plain.odd_unseen" = { fg = "mail.listing.compact.odd_unseen", bg = "mail.listing.compact.odd_unseen", attrs = "theme_default" }
"mail.listing.conversations" = { fg = "$dimmed_text", bg = "theme_default", attrs = "Default" }
"mail.listing.conversations.date" = { fg = "mail.listing.conversations", bg = "mail.listing.conversations", attrs = "theme_default" }
"mail.listing.conversations.from" = { fg = "mail.listing.conversations", bg = "mail.listing.conversations", attrs = "theme_default" }
"mail.listing.conversations.subject" = { fg = "mail.listing.conversations", bg = "mail.listing.conversations", attrs = "theme_default" }
"mail.listing.conversations.highlighted" = { fg = "theme_default", bg = "$dimmed_bg", attrs = "theme_default" }
"mail.listing.conversations.selected" = { fg = "theme_default", bg = "LightCoral", attrs = "theme_default" }
"mail.listing.conversations.unseen" = { fg = "$unseen_fg", bg = "$unseen_bg", attrs = "theme_default" }
"mail.listing.tag_default" = { fg = "Black", bg = "$dimmed_text", attrs = "theme_default" }
"mail.listing.thread_snooze_flag" = { fg = "Red", bg = "theme_default", attrs = "theme_default" }
"mail.sidebar" = { fg = "$dimmed_text", bg = "theme_default", attrs = "theme_default" }
"mail.sidebar_highlighted" = { fg = "theme_default", bg = "$dimmed_bg", attrs = "theme_default" }
"mail.sidebar_highlighted_account" = { fg = "$dimmed_text", bg = "theme_default", attrs = "theme_default" }
"mail.sidebar_highlighted_account_name" = { fg = "theme_default", bg = "$header", attrs = "Bold" }
"mail.sidebar_account_name" = { fg = "mail.sidebar", bg = "mail.sidebar", attrs = "theme_default" }
"mail.sidebar_highlighted_account_index" = { fg = "mail.sidebar_index", bg = "mail.sidebar_highlighted_account", attrs = "theme_default" }
"mail.sidebar_highlighted_account_unread_count" = { fg = "mail.sidebar_unread_count", bg = "mail.sidebar_highlighted_account", attrs = "theme_default" }
"mail.sidebar_highlighted_index" = { fg = "mail.sidebar_index", bg = "mail.sidebar_highlighted", attrs = "theme_default" }
"mail.sidebar_highlighted_unread_count" = { fg = "mail.sidebar_highlighted", bg = "mail.sidebar_highlighted", attrs = "theme_default" }
"mail.sidebar_index" = { fg = "mail.sidebar", bg = "mail.sidebar", attrs = "theme_default" }
"mail.sidebar_unread_count" = { fg = "$dimmed_text", bg = "theme_default", attrs = "theme_default" }
"mail.view.body" = { fg = "theme_default", bg = "theme_default", attrs = "theme_default" }
"mail.view.headers" = { fg = "theme_default", bg = "theme_default", attrs = "theme_default" }
"mail.view.headers_names" = { fg = "theme_default", bg = "theme_default", attrs = "Bold" }
"mail.view.thread.indentation.a" = { fg = "theme_default", bg = "#EC633D", attrs = "theme_default" }
"mail.view.thread.indentation.b" = { fg = "theme_default", bg = "#D347F9", attrs = "theme_default" }
"mail.view.thread.indentation.c" = { fg = "theme_default", bg = "#317EFB", attrs = "theme_default" }
"mail.view.thread.indentation.d" = { fg = "theme_default", bg = "#06B8CD", attrs = "theme_default" }
"mail.view.thread.indentation.e" = { fg = "theme_default", bg = "#93DDB6", attrs = "theme_default" }
"mail.view.thread.indentation.f" = { fg = "theme_default", bg = "#68A033", attrs = "theme_default" }
"pager.highlight_search" = { fg = "White", bg = "Teal", attrs = "Bold" }
"pager.highlight_search_current" = { fg = "White", bg = "NavyBlue", attrs = "Bold" }
"status.bar" = { fg = "theme_default", bg = "$sea", attrs = "theme_default" }
"status.notification" = { fg = "Plum1", bg = "theme_default", attrs = "theme_default" }
"tab.bar" = { fg = "theme_default", bg = "theme_default", attrs = "theme_default" }
"tab.focused" = { fg = "theme_default", bg = "theme_default", attrs = "Bold" }
"tab.unfocused" = { fg = "White", bg = "$sea", attrs = "Bold" }
"widgets.form.field" = { fg = "theme_default", bg = "theme_default", attrs = "theme_default" }
"widgets.form.highlighted" = { fg = "theme_default", bg = "$dimmed_bg", attrs = "theme_default" }
"widgets.form.label" = { fg = "theme_default", bg = "theme_default", attrs = "Bold" }
"widgets.list.header" = { fg = "Black", bg = "White", attrs = "Bold" }
"widgets.options.highlighted" = { fg = "theme_default", bg = "$dimmed_bg", attrs = "theme_default" }

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

View File

@ -1,6 +1,6 @@
[package]
name = "melib"
version = "0.6.1"
version = "0.6.2"
authors = ["Manos Pitsidianakis <epilys@nessuent.xyz>"]
workspace = ".."
edition = "2018"
@ -8,10 +8,11 @@ build = "build.rs"
homepage = "https://meli.delivery"
repository = "https://git.meli.delivery/meli/meli.git"
description = "backend mail client library"
keywords = ["mail", "mua", "maildir", "imap"]
categories = [ "email"]
description = "mail library"
keywords = ["mail", "mua", "maildir", "imap", "jmap"]
categories = [ "email", "parser-implementations"]
license = "GPL-3.0-or-later"
readme = "README.md"
[lib]
name = "melib"
@ -19,10 +20,8 @@ path = "src/lib.rs"
[dependencies]
bitflags = "1.0"
crossbeam = "0.7.2"
data-encoding = "2.1.1"
encoding = "0.2.33"
memmap = { version = "0.5.2", optional = true }
nom = { version = "5.1.1" }
indexmap = { version = "^1.5", features = ["serde-1", ] }
@ -31,39 +30,40 @@ xdg = "2.1.0"
native-tls = { version ="0.2.3", optional=true }
serde = { version = "1.0.71", features = ["rc", ] }
serde_derive = "1.0.71"
bincode = "1.2.0"
bincode = "^1.3.0"
uuid = { version = "0.8.1", features = ["serde", "v4", "v5"] }
unicode-segmentation = { version = "1.2.1", optional = true }
libc = {version = "0.2.59", features = ["extra_traits",]}
isahc = { version = "0.9.7", optional = true, default-features = false, features = ["http2", "json", "text-decoding"]}
serde_json = { version = "1.0", optional = true, features = ["raw_value",] }
smallvec = { version = "^1.4.0", features = ["serde", ] }
smallvec = { version = "^1.5.0", features = ["serde", ] }
nix = "0.17.0"
rusqlite = {version = "0.24.0", optional = true }
libloading = "0.6.2"
futures = "0.3.5"
smol = "0.1.18"
smol = "1.0.0"
async-stream = "0.2.1"
base64 = { version = "0.12.3", optional = true }
flate2 = { version = "1.0.16", optional = true }
xdg-utils = "0.3.0"
xdg-utils = "^0.4.0"
[features]
default = ["unicode_algorithms", "imap_backend", "maildir_backend", "mbox_backend", "vcard", "sqlite3", "smtp", "deflate_compression"]
debug-tracing = []
deflate_compression = ["flate2", ]
gpgme = []
http = ["isahc"]
http-static = ["isahc", "isahc/static-curl"]
tls = ["native-tls"]
imap_backend = ["tls"]
jmap_backend = ["http", "serde_json"]
maildir_backend = ["notify", "memmap"]
mbox_backend = ["notify", "memmap"]
maildir_backend = ["notify"]
mbox_backend = ["notify"]
notmuch_backend = []
smtp = ["tls", "base64"]
sqlite3 = ["rusqlite", ]
tls = ["native-tls"]
unicode_algorithms = ["unicode-segmentation"]
vcard = []

88
melib/README.md 100644
View File

@ -0,0 +1,88 @@
# melib
[![GitHub license](https://img.shields.io/github/license/meli/meli)](https://github.com/meli/meli/blob/master/COPYING) [![Crates.io](https://img.shields.io/crates/v/melib)](https://crates.io/crates/melib) [![docs.rs](https://docs.rs/melib/badge.svg)](https://docs.rs/melib)
Library for handling mail.
## optional features
| feature flag | dependencies | notes |
| ---------------------- | ----------------------------------- | ------------------------ |
| `imap_backend` | `native-tls` | |
| `deflate_compression` | `flate2` | for use with IMAP |
| `jmap_backend` | `isahc`, `native-tls`, `serde_json` | |
| `maildir_backend` | `notify` | |
| `mbox_backend` | `notify` | |
| `notmuch_backend` | `notify` | |
| `sqlite` | `rusqlite` | used in IMAP cache |
| `unicode_algorithms` | `unicode-segmentation` | linebreaking algo etc |
| `vcard` | | vcard parsing |
| `gpgme` | | GPG use with libgpgme |
| `smtp` | `native-tls`, `base64` | async SMTP communication |
## Example: Parsing bytes into an `Envelope`
An `Envelope` represents the information you can get from an email's headers
and body structure. Addresses in `To`, `From` fields etc are parsed into
`Address` types.
```rust
use melib::{Attachment, Envelope};
let raw_mail = r#"From: "some name" <some@example.com>
To: "me" <myself@example.com>
Cc:
Subject: =?utf-8?Q?gratuitously_encoded_subject?=
Message-ID: <h2g7f.z0gy2pgaen5m@example.com>
MIME-Version: 1.0
Content-Type: multipart/mixed; charset="utf-8";
boundary="bzz_bzz__bzz__"
This is a MIME formatted message with attachments. Use a MIME-compliant client to view it properly.
--bzz_bzz__bzz__
hello world.
--bzz_bzz__bzz__
Content-Type: image/gif; name="test_image.gif"; charset="utf-8"
Content-Disposition: attachment
Content-Transfer-Encoding: base64
R0lGODdhKAAXAOfZAAABzAADzQAEzgQFtBEAxAAGxBcAxwALvRcFwAAPwBcLugATuQEUuxoNuxYQ
sxwOvAYVvBsStSAVtx8YsRUcuhwhth4iuCQsyDAwuDc1vTc3uDg4uT85rkc9ukJBvENCvURGukdF
wUVKt0hLuUxPvVZSvFlYu1hbt2BZuFxdul5joGhqlnNuf3FvlnBvwXJyt3Jxw3N0oXx1gH12gV99
z317f3N7spFxwHp5wH99gYB+goF/g25+26tziIOBhWqD3oiBjICAuudkjIN+zHeC2n6Bzc1vh4eF
iYaBw8F0kImHi4KFxYyHmIWIvI2Lj4uIvYaJyY+IuJGMi5iJl4qKxZSMmIuLxpONnpGPk42NvI2M
1LKGl46OvZePm5ORlZiQnJqSnpaUmLyJnJuTn5iVmZyUoJGVyZ2VoZSVw5iXoZmWrO18rJiUyp6W
opuYnKaVnZ+Xo5yZncaMoaCYpJiaqo+Z2Z2annuf5qGZpa2WoJybpZmayZ2Z0KCZypydrZ6dp6Cd
oZ6a0aGay5ucy5+eqKGeouWMgp+b0qKbzKCfqdqPnp2ezaGgqqOgpKafqrScpp+gz6ajqKujr62j
qayksKmmq62lsaiosqqorOyWnaqqtKeqzLGptaurta2rr7Kqtq+ssLOrt6+uuLGusuqhfbWtubCv
ubKvs7GwurOwtPSazbevu+ali7SxtbiwvOykjLOyvLWytuCmqOankrSzvbazuLmyvrW0vre0uba1
wLi1ury0wLm2u721wbe3wbq3vMC2vLi4wr+3w7m5w8C4xLi6yry6vsG5xbu7xcC6zMK6xry8xry+
u8O7x729x8C9wb++yMG+wsO+vMK/w8a+y8e/zMnBzcXH18nL2///////////////////////////
////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////ywAAAAAKAAXAAAI/gBP4Cjh
IYMLEh0w4EgBgsMLEyFGFBEB5cOFABgzatS4AVssZAOsLOHCxooVMzCyoNmzaBOkJlS0VEDyZMjG
mxk3XOMF60CDBgsoPABK9KcDCRImPCiQYAECAgQCRMU4VSrGCjFarBgUSJCgQ10FBTrkNRCfPnz4
dA3UNa1btnDZqgU7Ntqzu3ej2X2mFy9eaHuhNRtMGJrhwYYN930G2K7eaNIY34U2mfJkwpgzI9Yr
GBqwR2KSvAlMOXHnw5pTNzPdLNoWIWtU9XjGjDEYS8LAlFm1SrVvzIKj5TH0KpORSZOryPgCZgqL
Ob+jG0YVRBErUrOiiGJ8KxgtYsh27xWL/tswnTtEbsiRVYdJNMHk4yOGhswGjR88UKjQ9Ey+/8TL
XKKGGn7Akph/8XX2WDTTcAYfguVt9hhrEPqmzIOJ3VUheb48WJiHG6amC4i+WVJKKCimqGIoYxyj
WWK8kKjaJ9bA18sxvXjYhourmbbMMrjI+OIn1QymDCVXANGFK4S1gQw0PxozzC+33FLLKUJq9gk1
gyWDhyNwrMLkYGUEM4wvuLRiCiieXIJJJVlmJskcZ9TZRht1lnFGGmTMkMoonVQSSSOFAGJHHI0w
ouiijDaaCCGQRgrpH3q4QYYXWDihxBE+7KCDDjnUIEVAADs=
--bzz_bzz__bzz__--"#;
let envelope = Envelope::from_bytes(raw_mail.as_bytes(), None).expect("Could not parse mail");
assert_eq!(envelope.subject().as_ref(), "gratuitously encoded subject");
assert_eq!(envelope.message_id_display().as_ref(), "<h2g7f.z0gy2pgaen5m@example.com>");
let body = envelope.body_bytes(raw_mail.as_bytes());
assert_eq!(body.content_type().to_string().as_str(), "multipart/mixed");
let body_text = body.text();
assert_eq!(body_text.as_str(), "hello world.");
let subattachments: Vec<Attachment> = body.attachments();
assert_eq!(subattachments.len(), 3);
assert_eq!(subattachments[2].content_type().name().unwrap(), "test_image.gif");
```

View File

@ -25,15 +25,26 @@ include!("src/text_processing/types.rs");
fn main() -> Result<(), std::io::Error> {
#[cfg(feature = "unicode_algorithms")]
{
const MOD_PATH: &str = "src/text_processing/tables.rs";
println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rerun-if-changed={}", MOD_PATH);
/* Line break tables */
use std::fs::File;
use std::io::prelude::*;
use std::io::BufReader;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::path::Path;
use std::process::{Command, Stdio};
const LINE_BREAK_TABLE_URL: &str =
"http://www.unicode.org/Public/UCD/latest/ucd/LineBreak.txt";
/* Grapheme width tables */
const UNICODE_DATA_URL: &str =
"http://www.unicode.org/Public/UCD/latest/ucd/UnicodeData.txt";
const EAW_URL: &str = "http://www.unicode.org/Public/UCD/latest/ucd/EastAsianWidth.txt";
const EMOJI_DATA_URL: &str =
"https://www.unicode.org/Public/UCD/latest/ucd/emoji/emoji-data.txt";
let mod_path = Path::new("src/text_processing/tables.rs");
let mod_path = Path::new(MOD_PATH);
if mod_path.exists() {
eprintln!(
"{} already exists, delete it if you want to replace it.",
@ -41,18 +52,14 @@ fn main() -> Result<(), std::io::Error> {
);
std::process::exit(0);
}
let mut tmpdir_path = PathBuf::from(
std::str::from_utf8(&Command::new("mktemp").arg("-d").output()?.stdout)
.unwrap()
.trim(),
);
tmpdir_path.push("LineBreak.txt");
Command::new("curl")
.args(&["-o", tmpdir_path.to_str().unwrap(), LINE_BREAK_TABLE_URL])
.output()?;
let mut child = Command::new("curl")
.args(&["-o", "-", LINE_BREAK_TABLE_URL])
.stdout(Stdio::piped())
.stdin(Stdio::null())
.stderr(Stdio::inherit())
.spawn()?;
let file = File::open(&tmpdir_path)?;
let buf_reader = BufReader::new(file);
let buf_reader = BufReader::new(child.stdout.take().unwrap());
let mut line_break_table: Vec<(u32, u32, LineBreakClass)> = Vec::with_capacity(3800);
for line in buf_reader.lines() {
@ -69,31 +76,351 @@ fn main() -> Result<(), std::io::Error> {
let mut codepoint_iter = chars_str.split("..");
let first_codepoint: u32 =
u32::from_str_radix(std::dbg!(codepoint_iter.next().unwrap()), 16).unwrap();
u32::from_str_radix(codepoint_iter.next().unwrap(), 16).unwrap();
let sec_codepoint: u32 = codepoint_iter
.next()
.map(|v| u32::from_str_radix(std::dbg!(v), 16).unwrap())
.map(|v| u32::from_str_radix(v, 16).unwrap())
.unwrap_or(first_codepoint);
let class = &tokens[semicolon_idx + 1..semicolon_idx + 1 + 2];
line_break_table.push((first_codepoint, sec_codepoint, LineBreakClass::from(class)));
}
child.wait()?;
let child = Command::new("curl")
.args(&["-o", "-", UNICODE_DATA_URL])
.stdout(Stdio::piped())
.output()?;
let unicode_data = String::from_utf8_lossy(&child.stdout);
let child = Command::new("curl")
.args(&["-o", "-", EAW_URL])
.stdout(Stdio::piped())
.output()?;
let eaw_data = String::from_utf8_lossy(&child.stdout);
let child = Command::new("curl")
.args(&["-o", "-", EMOJI_DATA_URL])
.stdout(Stdio::piped())
.output()?;
let emoji_data = String::from_utf8_lossy(&child.stdout);
const MAX_CODEPOINT: usize = 0x110000;
// See https://www.unicode.org/L2/L1999/UnicodeData.html
const FIELD_CODEPOINT: usize = 0;
const FIELD_CATEGORY: usize = 2;
// Ambiguous East Asian characters
const WIDTH_AMBIGUOUS_EASTASIAN: isize = -3;
// Width changed from 1 to 2 in Unicode 9.0
const WIDTH_WIDENED_IN_9: isize = -6;
// Category for unassigned codepoints.
const CAT_UNASSIGNED: &str = "Cn";
// Category for private use codepoints.
const CAT_PRIVATE_USE: &str = "Co";
// Category for surrogates.
const CAT_SURROGATE: &str = "Cs";
struct Codepoint<'cat> {
raw: u32,
width: Option<isize>,
category: &'cat str,
}
let mut codepoints: Vec<Codepoint> = Vec::with_capacity(MAX_CODEPOINT + 1);
for i in 0..=MAX_CODEPOINT {
codepoints.push(Codepoint {
raw: i as u32,
width: None,
category: CAT_UNASSIGNED,
});
}
set_general_categories(&mut codepoints, &unicode_data);
set_eaw_widths(&mut codepoints, &eaw_data);
set_emoji_widths(&mut codepoints, &emoji_data);
set_hardcoded_ranges(&mut codepoints);
fn hexrange_to_range(hexrange: &str) -> std::ops::Range<usize> {
/* Given a string like 1F300..1F320 representing an inclusive range,
return the range of codepoints.
If the string is like 1F321, return a range of just that element.
*/
let hexrange = hexrange.trim();
let fields = hexrange
.split("..")
.map(|h| usize::from_str_radix(h.trim(), 16).unwrap())
.collect::<Vec<usize>>();
if fields.len() == 1 {
fields[0]..(fields[0] + 1)
} else {
fields[0]..(fields[1] + 1)
}
}
fn set_general_categories<'u>(codepoints: &mut Vec<Codepoint<'u>>, unicode_data: &'u str) {
for line in unicode_data.lines() {
let fields = line.trim().split(";").collect::<Vec<_>>();
if fields.len() > FIELD_CATEGORY {
for idx in hexrange_to_range(fields[FIELD_CODEPOINT]) {
codepoints[idx].category = fields[FIELD_CATEGORY];
}
}
}
}
fn set_eaw_widths(codepoints: &mut Vec<Codepoint<'_>>, eaw_data_lines: &str) {
// Read from EastAsianWidth.txt, set width values on the codepoints
for line in eaw_data_lines.lines() {
let line = line.trim().split('#').next().unwrap_or(line);
let fields = line.trim().split(';').collect::<Vec<_>>();
if fields.len() != 2 {
continue;
}
let hexrange = fields[0];
let width_type = fields[1];
// width_types:
// A: ambiguous, F: fullwidth, H: halfwidth,
// . N: neutral, Na: east-asian Narrow
let width: isize = if width_type == "A" {
WIDTH_AMBIGUOUS_EASTASIAN
} else if width_type == "F" || width_type == "W" {
2
} else {
1
};
for cp in hexrange_to_range(hexrange) {
codepoints[cp].width = Some(width);
}
}
// Apply the following special cases:
// - The unassigned code points in the following blocks default to "W":
// CJK Unified Ideographs Extension A: U+3400..U+4DBF
// CJK Unified Ideographs: U+4E00..U+9FFF
// CJK Compatibility Ideographs: U+F900..U+FAFF
// - All undesignated code points in Planes 2 and 3, whether inside or
// outside of allocated blocks, default to "W":
// Plane 2: U+20000..U+2FFFD
// Plane 3: U+30000..U+3FFFD
const WIDE_RANGES: [(usize, usize); 5] = [
(0x3400, 0x4DBF),
(0x4E00, 0x9FFF),
(0xF900, 0xFAFF),
(0x20000, 0x2FFFD),
(0x30000, 0x3FFFD),
];
for &wr in WIDE_RANGES.iter() {
for cp in wr.0..(wr.1 + 1) {
if codepoints[cp].width.is_none() {
codepoints[cp].width = Some(2);
}
}
}
}
fn set_emoji_widths(codepoints: &mut Vec<Codepoint<'_>>, emoji_data_lines: &str) {
// Read from emoji-data.txt, set codepoint widths
for line in emoji_data_lines.lines() {
if !line.contains("#") || line.trim().starts_with("#") {
continue;
}
let mut fields = line.trim().split('#').collect::<Vec<_>>();
if fields.len() != 2 {
continue;
}
let comment = fields.pop().unwrap();
let fields = fields.pop().unwrap();
let hexrange = fields.split(";").next().unwrap();
// In later versions of emoji-data.txt there are some "reserved"
// entries that have "NA" instead of a Unicode version number
// of first use, they will now return a zero version instead of
// crashing the script
if comment.trim().starts_with("NA") {
continue;
}
use std::str::FromStr;
let mut v = comment.trim().split_whitespace().next().unwrap();
if v.starts_with("E") {
v = &v[1..];
}
if v.as_bytes()
.get(0)
.map(|c| !c.is_ascii_digit())
.unwrap_or(true)
{
continue;
}
let mut idx = 1;
while v
.as_bytes()
.get(idx)
.map(|c| c.is_ascii_digit())
.unwrap_or(false)
{
idx += 1;
}
if v.as_bytes().get(idx).map(|&c| c != b'.').unwrap_or(true) {
continue;
}
idx += 1;
while v
.as_bytes()
.get(idx)
.map(|c| c.is_ascii_digit())
.unwrap_or(false)
{
idx += 1;
}
v = &v[0..idx];
let version = f32::from_str(v).unwrap();
for cp in hexrange_to_range(hexrange) {
// Don't consider <=1F000 values as emoji. These can only be made
// emoji through the variation selector which interacts terribly
// with wcwidth().
if cp < 0x1F000 {
continue;
}
// Skip codepoints that are explicitly not wide.
// For example U+1F336 ("Hot Pepper") renders like any emoji but is
// marked as neutral in EAW so has width 1 for some reason.
//if codepoints[cp].width == Some(1) {
// continue;
//}
// If this emoji was introduced before Unicode 9, then it was widened in 9.
codepoints[cp].width = if version >= 9.0 {
Some(2)
} else {
Some(WIDTH_WIDENED_IN_9)
};
}
}
}
fn set_hardcoded_ranges(codepoints: &mut Vec<Codepoint<'_>>) {
// Mark private use and surrogate codepoints
// Private use can be determined awkwardly from UnicodeData.txt,
// but we just hard-code them.
// We do not treat "private use high surrogate" as private use
// so as to match wcwidth9().
const PRIVATE_RANGES: [(usize, usize); 3] =
[(0xE000, 0xF8FF), (0xF0000, 0xFFFFD), (0x100000, 0x10FFFD)];
for &(first, last) in PRIVATE_RANGES.iter() {
for idx in first..=last {
codepoints[idx].category = CAT_PRIVATE_USE;
}
}
const SURROGATE_RANGES: [(usize, usize); 2] = [(0xD800, 0xDBFF), (0xDC00, 0xDFFF)];
for &(first, last) in SURROGATE_RANGES.iter() {
for idx in first..=last {
codepoints[idx].category = CAT_SURROGATE;
}
}
}
let mut file = File::create(&mod_path)?;
file.write_all(b"use crate::types::LineBreakClass::*;\n")
.unwrap();
file.write_all(b"use crate::types::LineBreakClass;\n\n")
.unwrap();
file.write_all(b"const LINE_BREAK_RULES: &[(u32, u32, LineBreakClass)] = &[\n")
.unwrap();
file.write_all(
br#"/*
* meli - text_processing crate.
*
* Copyright 2017-2020 Manos Pitsidianakis
*
* This file is part of meli.
*
* meli is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* meli is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
use super::types::LineBreakClass::{self, *};
pub const LINE_BREAK_RULES: &[(u32, u32, LineBreakClass)] = &[
"#,
)
.unwrap();
for l in &line_break_table {
file.write_all(format!(" (0x{:X}, 0x{:X}, {:?}),\n", l.0, l.1, l.2).as_bytes())
.unwrap();
}
file.write_all(b"];").unwrap();
std::fs::remove_file(&tmpdir_path).unwrap();
tmpdir_path.pop();
std::fs::remove_dir(&tmpdir_path).unwrap();
file.write_all(b"];\n").unwrap();
for (name, filter) in [
(
"ASCII",
Box::new(|c: &&Codepoint| c.raw < 0x7f && c.raw >= 0x20)
as Box<dyn Fn(&&Codepoint) -> bool>,
),
(
"PRIVATE",
Box::new(|c: &&Codepoint| c.category == CAT_PRIVATE_USE),
),
(
"NONPRINT",
Box::new(|c: &&Codepoint| {
["Cc", "Cf", "Zl", "Zp", CAT_SURROGATE].contains(&c.category)
}),
),
(
"COMBINING",
Box::new(|c: &&Codepoint| ["Mn", "Mc", "Me"].contains(&c.category)),
),
("DOUBLEWIDE", Box::new(|c: &&Codepoint| c.width == Some(2))),
(
"UNASSIGNED",
Box::new(|c: &&Codepoint| c.category == CAT_UNASSIGNED),
),
(
"AMBIGUOUS",
Box::new(|c: &&Codepoint| c.width == Some(WIDTH_AMBIGUOUS_EASTASIAN)),
),
(
"WIDENEDIN9",
Box::new(|c: &&Codepoint| c.width == Some(WIDTH_WIDENED_IN_9)),
),
]
.iter()
{
file.write_all(
format!(
r#"
pub const {}: &[(u32, u32)] = &[
"#,
name
)
.as_bytes(),
)
.unwrap();
let mut iter = codepoints.iter().filter(filter);
let mut prev = iter.next().unwrap().raw;
let mut a = prev;
for cp in iter {
if prev + 1 != cp.raw {
file.write_all(format!(" (0x{:X}, 0x{:X}),\n", a, prev).as_bytes())
.unwrap();
a = cp.raw;
}
prev = cp.raw;
}
file.write_all(format!(" (0x{:X}, 0x{:X}),\n", a, prev).as_bytes())
.unwrap();
file.write_all(b"];\n").unwrap();
}
}
Ok(())
}

View File

@ -192,7 +192,7 @@ impl Card {
self.key.as_str()
}
pub fn last_edited(&self) -> String {
datetime::timestamp_to_string(self.last_edited, None)
datetime::timestamp_to_string(self.last_edited, None, false)
}
pub fn set_id(&mut self, new_val: CardId) {

View File

@ -201,7 +201,7 @@ impl<V: VCardVersion> TryInto<Card> for VCard<V> {
T102200Z
T102200-0800
*/
card.birthday = crate::datetime::timestamp_from_string(val.value.as_str(), "%Y%m%d")
card.birthday = crate::datetime::timestamp_from_string(val.value.as_str(), "%Y%m%d\0")
.unwrap_or_default();
}
if let Some(val) = self.0.remove("EMAIL") {

View File

@ -58,15 +58,15 @@ use self::maildir::MaildirType;
use self::mbox::MboxType;
use super::email::{Envelope, EnvelopeHash, Flag};
use std::any::Any;
use std::collections::BTreeMap;
use std::collections::BTreeSet;
use std::fmt;
use std::fmt::Debug;
use std::ops::Deref;
use std::sync::{Arc, RwLock};
pub use futures::stream::Stream;
use futures::stream::Stream;
use std::future::Future;
pub use std::pin::Pin;
use std::pin::Pin;
use std::collections::HashMap;
@ -246,6 +246,14 @@ pub enum RefreshEventKind {
NewFlags(EnvelopeHash, (Flag, Vec<String>)),
Rescan,
Failure(MeliError),
MailboxCreate(Mailbox),
MailboxDelete(MailboxHash),
MailboxRename {
old_mailbox_hash: MailboxHash,
new_mailbox: Mailbox,
},
MailboxSubscribe(MailboxHash),
MailboxUnsubscribe(MailboxHash),
}
#[derive(Debug, Clone)]
@ -335,18 +343,12 @@ pub trait MailBackend: ::std::fmt::Debug + Send + Sync {
) -> ResultFuture<()>;
fn delete_messages(
&self,
_env_hashes: EnvelopeHashBatch,
_mailbox_hash: MailboxHash,
) -> ResultFuture<()> {
Err(MeliError::new("Unimplemented."))
}
fn delete(&self, _env_hash: EnvelopeHash, _mailbox_hash: MailboxHash) -> ResultFuture<()> {
Err(MeliError::new("Unimplemented."))
}
fn tags(&self) -> Option<Arc<RwLock<BTreeMap<u64, String>>>> {
None
}
&mut self,
env_hashes: EnvelopeHashBatch,
mailbox_hash: MailboxHash,
) -> ResultFuture<()>;
fn collection(&self) -> crate::Collection;
fn as_any(&self) -> &dyn Any;
fn as_any_mut(&mut self) -> &mut dyn Any;
@ -617,3 +619,95 @@ impl EnvelopeHashBatch {
1 + self.rest.len()
}
}
#[derive(Default, Clone)]
pub struct LazyCountSet {
not_yet_seen: usize,
set: BTreeSet<EnvelopeHash>,
}
impl fmt::Debug for LazyCountSet {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.debug_struct("LazyCountSet")
.field("not_yet_seen", &self.not_yet_seen)
.field("set", &self.set.len())
.field("total_len", &self.len())
.finish()
}
}
impl LazyCountSet {
pub fn set_not_yet_seen(&mut self, new_val: usize) {
self.not_yet_seen = new_val;
}
pub fn insert_existing(&mut self, new_val: EnvelopeHash) -> bool {
if self.not_yet_seen == 0 {
false
} else {
if !self.set.contains(&new_val) {
self.not_yet_seen -= 1;
}
self.set.insert(new_val);
true
}
}
pub fn insert_existing_set(&mut self, set: BTreeSet<EnvelopeHash>) {
let old_len = self.set.len();
self.set.extend(set.into_iter());
self.not_yet_seen = self.not_yet_seen.saturating_sub(self.set.len() - old_len);
}
#[inline(always)]
pub fn len(&self) -> usize {
self.set.len() + self.not_yet_seen
}
#[inline(always)]
pub fn clear(&mut self) {
self.set.clear();
self.not_yet_seen = 0;
}
pub fn insert_new(&mut self, new_val: EnvelopeHash) {
self.set.insert(new_val);
}
pub fn insert_set(&mut self, set: BTreeSet<EnvelopeHash>) {
self.set.extend(set.into_iter());
}
pub fn remove(&mut self, env_hash: EnvelopeHash) -> bool {
self.set.remove(&env_hash)
}
}
#[test]
fn test_lazy_count_set() {
let mut new = LazyCountSet::default();
assert_eq!(new.len(), 0);
new.set_not_yet_seen(10);
assert_eq!(new.len(), 10);
for i in 0..10 {
assert!(new.insert_existing(i));
}
assert_eq!(new.len(), 10);
assert!(!new.insert_existing(10));
assert_eq!(new.len(), 10);
}
pub struct IsSubscribedFn(Box<dyn Fn(&str) -> bool + Send + Sync>);
impl std::fmt::Debug for IsSubscribedFn {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "IsSubscribedFn Box")
}
}
impl std::ops::Deref for IsSubscribedFn {
type Target = Box<dyn Fn(&str) -> bool + Send + Sync>;
fn deref(&self) -> &Box<dyn Fn(&str) -> bool + Send + Sync> {
&self.0
}
}

View File

@ -42,20 +42,21 @@ use crate::backends::{
*,
};
use crate::collection::Collection;
use crate::conf::AccountSettings;
use crate::connections::timeout;
use crate::email::{parser::BytesExt, *};
use crate::error::{MeliError, Result, ResultIntoMeliError};
use futures::lock::Mutex as FutureMutex;
use futures::stream::Stream;
use std::collections::{hash_map::DefaultHasher, BTreeMap};
use std::collections::hash_map::DefaultHasher;
use std::collections::{BTreeSet, HashMap, HashSet};
use std::convert::TryFrom;
use std::convert::TryInto;
use std::hash::Hasher;
use std::pin::Pin;
use std::str::FromStr;
use std::sync::{Arc, Mutex, RwLock};
use std::sync::{Arc, Mutex};
use std::time::{Duration, SystemTime};
pub type ImapNum = usize;
@ -64,6 +65,7 @@ pub type UIDVALIDITY = UID;
pub type MessageSequenceNumber = ImapNum;
pub static SUPPORTED_CAPABILITIES: &[&str] = &[
"AUTH=OAUTH2",
#[cfg(feature = "deflate_compression")]
"COMPRESS=DEFLATE",
"CONDSTORE",
@ -82,9 +84,7 @@ pub static SUPPORTED_CAPABILITIES: &[&str] = &[
#[derive(Debug, Default)]
pub struct EnvelopeCache {
bytes: Option<String>,
headers: Option<String>,
body: Option<String>,
bytes: Option<Vec<u8>>,
flags: Option<Flag>,
}
@ -101,20 +101,6 @@ pub struct ImapServerConf {
pub timeout: Option<Duration>,
}
struct IsSubscribedFn(Box<dyn Fn(&str) -> bool + Send + Sync>);
impl std::fmt::Debug for IsSubscribedFn {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "IsSubscribedFn Box")
}
}
impl std::ops::Deref for IsSubscribedFn {
type Target = Box<dyn Fn(&str) -> bool + Send + Sync>;
fn deref(&self) -> &Box<dyn Fn(&str) -> bool + Send + Sync> {
&self.0
}
}
type Capabilities = HashSet<Vec<u8>>;
#[macro_export]
@ -157,14 +143,13 @@ pub struct UIDStore {
msn_index: Arc<Mutex<HashMap<MailboxHash, Vec<UID>>>>,
byte_cache: Arc<Mutex<HashMap<UID, EnvelopeCache>>>,
tag_index: Arc<RwLock<BTreeMap<u64, String>>>,
collection: Collection,
/* Offline caching */
uidvalidity: Arc<Mutex<HashMap<MailboxHash, UID>>>,
envelopes: Arc<Mutex<HashMap<EnvelopeHash, cache::CachedEnvelope>>>,
max_uids: Arc<Mutex<HashMap<MailboxHash, UID>>>,
modseq: Arc<Mutex<HashMap<EnvelopeHash, ModSequence>>>,
reverse_modseq: Arc<Mutex<HashMap<MailboxHash, BTreeMap<ModSequence, EnvelopeHash>>>>,
highestmodseqs: Arc<Mutex<HashMap<MailboxHash, std::result::Result<ModSequence, ()>>>>,
mailboxes: Arc<FutureMutex<HashMap<MailboxHash, ImapMailbox>>>,
is_online: Arc<Mutex<(SystemTime, Result<()>)>>,
@ -188,14 +173,13 @@ impl UIDStore {
envelopes: Default::default(),
max_uids: Default::default(),
modseq: Default::default(),
reverse_modseq: Default::default(),
highestmodseqs: Default::default(),
hash_index: Default::default(),
uid_index: Default::default(),
msn_index: Default::default(),
byte_cache: Default::default(),
mailboxes: Arc::new(FutureMutex::new(Default::default())),
tag_index: Arc::new(RwLock::new(Default::default())),
collection: Default::default(),
is_online: Arc::new(Mutex::new((
SystemTime::now(),
Err(MeliError::new("Account is uninitialised.")),
@ -236,6 +220,7 @@ impl MailBackend for ImapType {
#[cfg(feature = "deflate_compression")]
deflate,
condstore,
oauth2,
},
} = self.server_conf.protocol
{
@ -277,6 +262,15 @@ impl MailBackend for ImapType {
};
}
}
"AUTH=OAUTH2" => {
if oauth2 {
*status = MailBackendExtensionStatus::Enabled { comment: None };
} else {
*status = MailBackendExtensionStatus::Supported {
comment: Some("Disabled by user configuration"),
};
}
}
_ => {
if SUPPORTED_CAPABILITIES
.iter()
@ -325,7 +319,7 @@ impl MailBackend for ImapType {
None
};
let mut state = FetchState {
stage: if self.uid_store.keep_offline_cache {
stage: if self.uid_store.keep_offline_cache && cache_handle.is_some() {
FetchStage::InitialCache
} else {
FetchStage::InitialFresh
@ -336,11 +330,24 @@ impl MailBackend for ImapType {
cache_handle,
};
/* do this in a closure to prevent recursion limit error in async_stream macro */
let prepare_cl = |f: &ImapMailbox| {
f.set_warm(true);
if let Ok(mut exists) = f.exists.lock() {
let total = exists.len();
exists.clear();
exists.set_not_yet_seen(total);
}
if let Ok(mut unseen) = f.unseen.lock() {
let total = unseen.len();
unseen.clear();
unseen.set_not_yet_seen(total);
}
};
Ok(Box::pin(async_stream::try_stream! {
{
let f = &state.uid_store.mailboxes.lock().await[&mailbox_hash];
f.exists.lock().unwrap().clear();
f.unseen.lock().unwrap().clear();
prepare_cl(f);
if f.no_select {
yield vec![];
return;
@ -430,11 +437,11 @@ impl MailBackend for ImapType {
match timeout(timeout_dur, connection.lock()).await {
Ok(mut conn) => {
debug!("is_online");
match debug!(timeout(timeout_dur, conn.connect()).await) {
match timeout(timeout_dur, conn.connect()).await {
Ok(Ok(())) => Ok(()),
Err(err) | Ok(Err(err)) => {
conn.stream = Err(err.clone());
debug!(conn.connect().await)
conn.connect().await
}
}
}
@ -447,21 +454,20 @@ impl MailBackend for ImapType {
let server_conf = self.server_conf.clone();
let main_conn = self.connection.clone();
let uid_store = self.uid_store.clone();
let has_idle: bool = match self.server_conf.protocol {
ImapProtocol::IMAP {
extension_use: ImapExtensionUse { idle, .. },
} => {
idle && uid_store
.capabilities
.lock()
.unwrap()
.iter()
.any(|cap| cap.eq_ignore_ascii_case(b"IDLE"))
}
_ => false,
};
Ok(Box::pin(async move {
debug!(has_idle);
let has_idle: bool = match server_conf.protocol {
ImapProtocol::IMAP {
extension_use: ImapExtensionUse { idle, .. },
} => {
idle && uid_store
.capabilities
.lock()
.unwrap()
.iter()
.any(|cap| cap.eq_ignore_ascii_case(b"IDLE"))
}
_ => false,
};
while let Err(err) = if has_idle {
idle(ImapWatchKit {
conn: ImapConnection::new_connection(&server_conf, uid_store.clone()),
@ -480,17 +486,19 @@ impl MailBackend for ImapType {
let mut main_conn_lck = timeout(uid_store.timeout, main_conn.lock()).await?;
if err.kind.is_network() {
uid_store.is_online.lock().unwrap().1 = Err(err.clone());
} else {
return Err(err);
}
debug!("failure: {}", err.to_string());
debug!("Watch failure: {}", err.to_string());
match timeout(uid_store.timeout, main_conn_lck.connect())
.await
.and_then(|res| res)
{
Err(err2) => {
debug!("reconnect attempt failed: {}", err2.to_string());
debug!("Watch reconnect attempt failed: {}", err2.to_string());
}
Ok(()) => {
debug!("reconnect attempt succesful");
debug!("Watch reconnect attempt succesful");
continue;
}
}
@ -624,25 +632,9 @@ impl MailBackend for ImapType {
}
let dest_path = {
let mailboxes = uid_store.mailboxes.lock().await;
let mailbox = mailboxes
.get(&source_mailbox_hash)
.ok_or_else(|| MeliError::new("Source mailbox not found"))?;
if move_ && !mailbox.permissions.lock().unwrap().delete_messages {
return Err(MeliError::new(format!(
"You are not allowed to delete messages from mailbox {}",
mailbox.path()
)));
}
let mailbox = mailboxes
.get(&destination_mailbox_hash)
.ok_or_else(|| MeliError::new("Destination mailbox not found"))?;
if !mailbox.permissions.lock().unwrap().create_messages {
return Err(MeliError::new(format!(
"You are not allowed to create messages in mailbox {}",
mailbox.path()
)));
}
mailbox.imap_path().to_string()
};
let mut response = Vec::with_capacity(8 * 1024);
@ -717,8 +709,9 @@ impl MailBackend for ImapType {
.await?;
if flags.iter().any(|(_, b)| *b) {
/* Set flags/tags to true */
let mut set_seen = false;
let command = {
let mut tag_lck = uid_store.tag_index.write().unwrap();
let mut tag_lck = uid_store.collection.tag_index.write().unwrap();
let mut cmd = format!("UID STORE {}", uids[0]);
for uid in uids.iter().skip(1) {
cmd = format!("{},{}", cmd, uid);
@ -740,6 +733,7 @@ impl MailBackend for ImapType {
}
Ok(flag) if *flag == Flag::SEEN => {
cmd.push_str("\\Seen ");
set_seen = true;
}
Ok(flag) if *flag == Flag::DRAFT => {
cmd.push_str("\\Draft ");
@ -766,8 +760,17 @@ impl MailBackend for ImapType {
conn.send_command(command.as_bytes()).await?;
conn.read_response(&mut response, RequiredResponses::empty())
.await?;
if set_seen {
let f = &uid_store.mailboxes.lock().await[&mailbox_hash];
if let Ok(mut unseen) = f.unseen.lock() {
for env_hash in env_hashes.iter() {
unseen.remove(env_hash);
}
};
}
}
if flags.iter().any(|(_, b)| !*b) {
let mut set_unseen = false;
/* Set flags/tags to false */
let command = {
let mut cmd = format!("UID STORE {}", uids[0]);
@ -791,6 +794,7 @@ impl MailBackend for ImapType {
}
Ok(flag) if *flag == Flag::SEEN => {
cmd.push_str("\\Seen ");
set_unseen = true;
}
Ok(flag) if *flag == Flag::DRAFT => {
cmd.push_str("\\Draft ");
@ -820,13 +824,40 @@ impl MailBackend for ImapType {
conn.send_command(command.as_bytes()).await?;
conn.read_response(&mut response, RequiredResponses::empty())
.await?;
if set_unseen {
let f = &uid_store.mailboxes.lock().await[&mailbox_hash];
if let Ok(mut unseen) = f.unseen.lock() {
for env_hash in env_hashes.iter() {
unseen.insert_new(env_hash);
}
};
}
}
Ok(())
}))
}
fn tags(&self) -> Option<Arc<RwLock<BTreeMap<u64, String>>>> {
Some(self.uid_store.tag_index.clone())
fn delete_messages(
&mut self,
env_hashes: EnvelopeHashBatch,
mailbox_hash: MailboxHash,
) -> ResultFuture<()> {
let flag_future = self.set_flags(
env_hashes,
mailbox_hash,
smallvec::smallvec![(Ok(Flag::TRASHED), true)],
)?;
let connection = self.connection.clone();
Ok(Box::pin(async move {
flag_future.await?;
let mut response = Vec::with_capacity(8 * 1024);
let mut conn = connection.lock().await;
conn.send_command("EXPUNGE".as_bytes()).await?;
conn.read_response(&mut response, RequiredResponses::empty())
.await?;
debug!("EXPUNGE response: {}", &String::from_utf8_lossy(&response));
Ok(())
}))
}
fn as_any(&self) -> &dyn Any {
@ -837,6 +868,10 @@ impl MailBackend for ImapType {
self
}
fn collection(&self) -> Collection {
self.uid_store.collection.clone()
}
fn create_mailbox(
&mut self,
mut path: String,
@ -1176,7 +1211,7 @@ impl MailBackend for ImapType {
let mut conn = connection.lock().await;
conn.examine_mailbox(mailbox_hash, &mut response, false)
.await?;
conn.send_command(format!("UID SEARCH CHARSET UTF-8 {}", query_str).as_bytes())
conn.send_command(format!("UID SEARCH CHARSET UTF-8 {}", query_str.trim()).as_bytes())
.await?;
conn.read_response(&mut response, RequiredResponses::SEARCH)
.await?;
@ -1215,7 +1250,14 @@ impl ImapType {
) -> Result<Box<dyn MailBackend>> {
let server_hostname = get_conf_val!(s["server_hostname"])?;
let server_username = get_conf_val!(s["server_username"])?;
let use_oauth2: bool = get_conf_val!(s["use_oauth2"], false)?;
let server_password = if !s.extra.contains_key("server_password_command") {
if use_oauth2 {
return Err(MeliError::new(format!(
"({}) `use_oauth2` use requires `server_password_command` set with a command that returns an OAUTH2 token. Consult documentation for guidance.",
s.name,
)));
}
get_conf_val!(s["server_password"])?.to_string()
} else {
let invocation = get_conf_val!(s["server_password_command"])?;
@ -1272,6 +1314,7 @@ impl ImapType {
condstore: get_conf_val!(s["use_condstore"], true)?,
#[cfg(feature = "deflate_compression")]
deflate: get_conf_val!(s["use_deflate"], true)?,
oauth2: use_oauth2,
},
},
timeout,
@ -1385,11 +1428,11 @@ impl ImapType {
conn.read_response(&mut res, RequiredResponses::LIST_REQUIRED)
.await?;
}
debug!("out: {}", String::from_utf8_lossy(&res));
let mut lines = res.split_rn();
/* Remove "M__ OK .." line */
lines.next_back();
for l in lines {
debug!("LIST reply: {}", String::from_utf8_lossy(&res));
for l in res.split_rn() {
if !l.starts_with(b"*") {
continue;
}
if let Ok(mut mailbox) = protocol_parser::list_mailbox_result(&l).map(|(_, v)| v) {
if let Some(parent) = mailbox.parent {
if mailboxes.contains_key(&parent) {
@ -1436,26 +1479,38 @@ impl ImapType {
conn.send_command(b"LSUB \"\" \"*\"").await?;
conn.read_response(&mut res, RequiredResponses::LSUB_REQUIRED)
.await?;
let mut lines = res.split_rn();
debug!("out: {}", String::from_utf8_lossy(&res));
/* Remove "M__ OK .." line */
lines.next_back();
for l in lines {
debug!("LSUB reply: {}", String::from_utf8_lossy(&res));
for l in res.split_rn() {
if !l.starts_with(b"*") {
continue;
}
if let Ok(subscription) = protocol_parser::list_mailbox_result(&l).map(|(_, v)| v) {
if let Some(f) = mailboxes.get_mut(&subscription.hash()) {
if f.special_usage() == SpecialUsageMailbox::Normal
&& subscription.special_usage() != SpecialUsageMailbox::Normal
{
f.set_special_usage(subscription.special_usage())?;
}
f.is_subscribed = true;
}
} else {
debug!("parse error for {:?}", l);
}
}
Ok(debug!(mailboxes))
Ok(mailboxes)
}
pub fn validate_config(s: &AccountSettings) -> Result<()> {
get_conf_val!(s["server_hostname"])?;
get_conf_val!(s["server_username"])?;
let use_oauth2: bool = get_conf_val!(s["use_oauth2"], false)?;
if !s.extra.contains_key("server_password_command") {
if use_oauth2 {
return Err(MeliError::new(format!(
"({}) `use_oauth2` use requires `server_password_command` set with a command that returns an OAUTH2 token. Consult documentation for guidance.",
s.name,
)));
}
get_conf_val!(s["server_password"])?;
} else if s.extra.contains_key("server_password") {
return Err(MeliError::new(format!(
@ -1616,7 +1671,7 @@ async fn fetch_hlpr(state: &mut FetchState) -> Result<Vec<Envelope>> {
FetchStage::ResyncCache => {
let mailbox_hash = state.mailbox_hash;
let mut conn = state.connection.lock().await;
let res = debug!(conn.resync(mailbox_hash).await);
let res = conn.resync(mailbox_hash).await;
if let Ok(Some(payload)) = res {
state.stage = FetchStage::Finished;
return Ok(payload);
@ -1648,26 +1703,21 @@ async fn fetch_hlpr(state: &mut FetchState) -> Result<Vec<Envelope>> {
return Ok(Vec::new());
}
let mut conn = connection.lock().await;
debug!("locked for fetch {}", mailbox_path);
let mut response = Vec::with_capacity(8 * 1024);
let max_uid_left = max_uid;
let chunk_size = 250;
let mut payload = vec![];
let mut envelopes = Vec::with_capacity(chunk_size);
conn.examine_mailbox(mailbox_hash, &mut response, false)
.await?;
if max_uid_left > 0 {
let mut envelopes = vec![];
debug!("{} max_uid_left= {}", mailbox_hash, max_uid_left);
let command = if max_uid_left == 1 {
"UID FETCH 1 (UID FLAGS ENVELOPE BODYSTRUCTURE)".to_string()
"UID FETCH 1 (UID FLAGS ENVELOPE BODY.PEEK[HEADER.FIELDS (REFERENCES)] BODYSTRUCTURE)".to_string()
} else {
format!(
"UID FETCH {}:{} (UID FLAGS ENVELOPE BODYSTRUCTURE)",
std::cmp::max(
"UID FETCH {}:{} (UID FLAGS ENVELOPE BODY.PEEK[HEADER.FIELDS (REFERENCES)] BODYSTRUCTURE)",
std::cmp::max(max_uid_left.saturating_sub(chunk_size), 1),
1
),
max_uid_left
)
};
@ -1681,29 +1731,43 @@ async fn fetch_hlpr(state: &mut FetchState) -> Result<Vec<Envelope>> {
mailbox_path
)
})?;
debug!(
"fetch response is {} bytes and {} lines",
response.len(),
String::from_utf8_lossy(&response).lines().count()
);
let (_, mut v, _) = protocol_parser::fetch_responses(&response)?;
debug!("responses len is {}", v.len());
debug!(
"fetch response is {} bytes and {} lines and has {} parsed Envelopes",
response.len(),
String::from_utf8_lossy(&response).lines().count(),
v.len()
);
for FetchResponse {
ref uid,
ref mut envelope,
ref mut flags,
ref raw_fetch_value,
ref references,
..
} in v.iter_mut()
{
if uid.is_none() || envelope.is_none() || flags.is_none() {
debug!("BUG? in fetch is none");
debug!(uid);
debug!(envelope);
debug!(flags);
debug!("response was: {}", String::from_utf8_lossy(&response));
debug!(conn.process_untagged(raw_fetch_value).await)?;
continue;
}
let uid = uid.unwrap();
let env = envelope.as_mut().unwrap();
env.set_hash(generate_envelope_hash(&mailbox_path, &uid));
let mut tag_lck = uid_store.tag_index.write().unwrap();
if let Some(value) = references {
env.set_references(value);
}
let mut tag_lck = uid_store.collection.tag_index.write().unwrap();
if let Some((flags, keywords)) = flags {
if !flags.intersects(Flag::SEEN) {
env.set_flags(*flags);
if !env.is_seen() {
our_unseen.insert(env.hash());
}
env.set_flags(*flags);
for f in keywords {
let hash = tag_hash!(f);
if !tag_lck.contains_key(&hash) {
@ -1714,14 +1778,14 @@ async fn fetch_hlpr(state: &mut FetchState) -> Result<Vec<Envelope>> {
}
}
if let Some(ref mut cache_handle) = cache_handle {
if let Err(err) = debug!(cache_handle
if let Err(err) = cache_handle
.insert_envelopes(mailbox_hash, &v)
.chain_err_summary(|| {
format!(
"Could not save envelopes in cache for mailbox {}",
mailbox_path
)
}))
})
{
(state.uid_store.event_consumer)(
state.uid_store.account_hash,
@ -1765,30 +1829,25 @@ async fn fetch_hlpr(state: &mut FetchState) -> Result<Vec<Envelope>> {
.lock()
.unwrap()
.insert((mailbox_hash, uid), env.hash());
envelopes.push((uid, env));
envelopes.push(env);
}
debug!("sending payload for {}", mailbox_hash);
unseen
unseen.lock().unwrap().insert_existing_set(our_unseen);
mailbox_exists
.lock()
.unwrap()
.insert_existing_set(our_unseen.iter().cloned().collect());
mailbox_exists.lock().unwrap().insert_existing_set(
envelopes.iter().map(|(_, env)| env.hash()).collect::<_>(),
);
.insert_existing_set(envelopes.iter().map(|env| env.hash()).collect::<_>());
drop(conn);
payload.extend(envelopes.into_iter().map(|(_, env)| env));
}
if max_uid_left <= 1 {
unseen.lock().unwrap().set_not_yet_seen(0);
mailbox_exists.lock().unwrap().set_not_yet_seen(0);
*stage = FetchStage::Finished;
} else {
*stage = FetchStage::FreshFetch {
max_uid: std::cmp::max(
std::cmp::max(max_uid_left.saturating_sub(chunk_size), 1),
1,
),
max_uid: std::cmp::max(max_uid_left.saturating_sub(chunk_size + 1), 1),
};
}
return Ok(payload);
return Ok(envelopes);
}
FetchStage::Finished => {
return Ok(vec![]);

View File

@ -140,7 +140,7 @@ mod sqlite3_m {
CREATE INDEX IF NOT EXISTS envelope_idx ON envelopes(hash);
CREATE INDEX IF NOT EXISTS mailbox_idx ON mailbox(mailbox_hash);",
),
version: 1,
version: 2,
};
impl ToSql for ModSequence {
@ -239,7 +239,7 @@ mod sqlite3_m {
.entry(mailbox_hash)
.and_modify(|entry| *entry = uidvalidity)
.or_insert(uidvalidity);
let mut tag_lck = self.uid_store.tag_index.write().unwrap();
let mut tag_lck = self.uid_store.collection.tag_index.write().unwrap();
for f in to_str!(&flags).split('\0') {
let hash = tag_hash!(f);
//debug!("hash {} flag {}", hash, &f);
@ -365,7 +365,7 @@ mod sqlite3_m {
fn envelopes(&mut self, mailbox_hash: MailboxHash) -> Result<Option<Vec<EnvelopeHash>>> {
debug!("envelopes mailbox_hash {}", mailbox_hash);
if debug!(self.mailbox_state(mailbox_hash)?.is_none()) {
if self.mailbox_state(mailbox_hash)?.is_none() {
return Ok(None);
}
@ -429,7 +429,6 @@ mod sqlite3_m {
.cloned()
.unwrap_or_default();
if self.mailbox_state(mailbox_hash)?.is_none() {
debug!(self.mailbox_state(mailbox_hash)?.is_none());
return Err(MeliError::new("Mailbox is not in cache").set_kind(ErrorKind::Bug));
}
let Self {
@ -445,7 +444,9 @@ mod sqlite3_m {
modseq,
flags: _,
body: _,
references: _,
envelope: Some(envelope),
raw_fetch_value: _,
} = item
{
max_uid = std::cmp::max(max_uid, *uid);
@ -469,13 +470,7 @@ mod sqlite3_m {
mailbox_hash: MailboxHash,
refresh_events: &[(UID, RefreshEvent)],
) -> Result<()> {
debug!(
"update with refresh_events mailbox_hash {} len {}",
mailbox_hash,
refresh_events.len()
);
if self.mailbox_state(mailbox_hash)?.is_none() {
debug!(self.mailbox_state(mailbox_hash)?.is_none());
return Err(MeliError::new("Mailbox is not in cache").set_kind(ErrorKind::Bug));
}
let Self {
@ -486,7 +481,7 @@ mod sqlite3_m {
let tx = connection.transaction()?;
let mut hash_index_lck = uid_store.hash_index.lock().unwrap();
for (uid, event) in refresh_events {
match debug!(&event.kind) {
match &event.kind {
RefreshEventKind::Remove(env_hash) => {
hash_index_lck.remove(&env_hash);
tx.execute(
@ -653,43 +648,16 @@ pub(super) async fn fetch_cached_envs(state: &mut FetchState) -> Result<Option<V
ref uid_store,
cache_handle: _,
} = state;
debug!(uid_store.keep_offline_cache);
let mailbox_hash = *mailbox_hash;
if !uid_store.keep_offline_cache {
return Ok(None);
}
{
let mut conn = connection.lock().await;
match debug!(conn.load_cache(mailbox_hash).await) {
match conn.load_cache(mailbox_hash).await {
None => return Ok(None),
Some(Ok(env_hashes)) => {
uid_store
.mailboxes
.lock()
.await
.entry(mailbox_hash)
.and_modify(|entry| {
entry
.exists
.lock()
.unwrap()
.insert_set(env_hashes.iter().cloned().collect());
let env_lck = uid_store.envelopes.lock().unwrap();
entry.unseen.lock().unwrap().insert_set(
env_hashes
.iter()
.filter_map(|h| {
if !env_lck[h].inner.is_seen() {
Some(*h)
} else {
None
}
})
.collect(),
);
});
let env_lck = uid_store.envelopes.lock().unwrap();
return Ok(Some(
env_hashes
.into_iter()
@ -699,7 +667,7 @@ pub(super) async fn fetch_cached_envs(state: &mut FetchState) -> Result<Option<V
.collect::<Vec<Envelope>>(),
));
}
Some(Err(err)) => return debug!(Err(err)),
Some(Err(err)) => return Err(err),
}
}
}

View File

@ -63,54 +63,20 @@ impl ImapConnection {
Ok(v) => v,
Err(err) => return Some(Err(err)),
};
match debug!(cache_handle.mailbox_state(mailbox_hash)) {
match cache_handle.mailbox_state(mailbox_hash) {
Err(err) => return Some(Err(err)),
Ok(Some(())) => {}
Ok(None) => {
return None;
}
};
match debug!(cache_handle.envelopes(mailbox_hash)) {
match cache_handle.envelopes(mailbox_hash) {
Ok(Some(envs)) => Some(Ok(envs)),
Ok(None) => None,
Err(err) => Some(Err(err)),
}
}
pub async fn build_cache(
&mut self,
cache_handle: &mut Box<dyn ImapCache>,
mailbox_hash: MailboxHash,
) -> Result<()> {
debug!("build_cache {}", mailbox_hash);
let mut response = Vec::with_capacity(8 * 1024);
// 1 get uidvalidity, highestmodseq
let select_response = self
.select_mailbox(mailbox_hash, &mut response, true)
.await?
.unwrap();
self.uid_store
.uidvalidity
.lock()
.unwrap()
.insert(mailbox_hash, select_response.uidvalidity);
if let Some(v) = select_response.highestmodseq {
self.uid_store
.highestmodseqs
.lock()
.unwrap()
.insert(mailbox_hash, v);
}
cache_handle.clear(mailbox_hash, &select_response)?;
self.send_command(b"UID FETCH 1:* (UID FLAGS ENVELOPE BODYSTRUCTURE)")
.await?;
self.read_response(&mut response, RequiredResponses::FETCH_REQUIRED)
.await?;
let fetches = protocol_parser::fetch_responses(&response)?.1;
cache_handle.insert_envelopes(mailbox_hash, &fetches)?;
Ok(())
}
//rfc4549_Synchronization_Operations_for_Disconnected_IMAP4_Clients
pub async fn resync_basic(
&mut self,
@ -150,16 +116,10 @@ impl ImapConnection {
)
};
let mut new_unseen = BTreeSet::default();
debug!("current_uidvalidity is {}", current_uidvalidity);
debug!("max_uid is {}", max_uid);
let select_response = self
.select_mailbox(mailbox_hash, &mut response, true)
.await?
.unwrap();
debug!(
"select_response.uidvalidity is {}",
select_response.uidvalidity
);
// 1. check UIDVALIDITY. If fail, discard cache and rebuild
if select_response.uidvalidity != current_uidvalidity {
cache_handle.clear(mailbox_hash, &select_response)?;
@ -170,7 +130,7 @@ impl ImapConnection {
// 2. tag1 UID FETCH <lastseenuid+1>:* <descriptors>
self.send_command(
format!(
"UID FETCH {}:* (UID FLAGS ENVELOPE BODYSTRUCTURE)",
"UID FETCH {}:* (UID FLAGS ENVELOPE BODY.PEEK[HEADER.FIELDS (REFERENCES)] BODYSTRUCTURE)",
max_uid + 1
)
.as_bytes(),
@ -189,18 +149,22 @@ impl ImapConnection {
ref uid,
ref mut envelope,
ref mut flags,
ref references,
..
} in v.iter_mut()
{
let uid = uid.unwrap();
let env = envelope.as_mut().unwrap();
env.set_hash(generate_envelope_hash(&mailbox_path, &uid));
let mut tag_lck = self.uid_store.tag_index.write().unwrap();
if let Some(value) = references {
env.set_references(value);
}
let mut tag_lck = self.uid_store.collection.tag_index.write().unwrap();
if let Some((flags, keywords)) = flags {
if !flags.intersects(Flag::SEEN) {
env.set_flags(*flags);
if !env.is_seen() {
new_unseen.insert(env.hash());
}
env.set_flags(*flags);
for f in keywords {
let hash = tag_hash!(f);
if !tag_lck.contains_key(&hash) {
@ -211,14 +175,14 @@ impl ImapConnection {
}
}
{
debug!(cache_handle
cache_handle
.insert_envelopes(mailbox_hash, &v)
.chain_err_summary(|| {
format!(
"Could not save envelopes in cache for mailbox {}",
mailbox_path
)
}))?;
})?;
}
for FetchResponse {
@ -252,14 +216,17 @@ impl ImapConnection {
payload.push((uid, env));
}
debug!("sending payload for {}", mailbox_hash);
unseen
.lock()
.unwrap()
.insert_existing_set(new_unseen.iter().cloned().collect());
mailbox_exists
.lock()
.unwrap()
.insert_existing_set(payload.iter().map(|(_, env)| env.hash()).collect::<_>());
let payload_hash_set: BTreeSet<_> =
payload.iter().map(|(_, env)| env.hash()).collect::<_>();
{
let mut unseen_lck = unseen.lock().unwrap();
for &seen_env_hash in payload_hash_set.difference(&new_unseen) {
unseen_lck.remove(seen_env_hash);
}
unseen_lck.insert_set(new_unseen);
}
mailbox_exists.lock().unwrap().insert_set(payload_hash_set);
// 3. tag2 UID FETCH 1:<lastseenuid> FLAGS
if max_uid == 0 {
self.send_command("UID FETCH 1:* FLAGS".as_bytes()).await?;
@ -370,9 +337,6 @@ impl ImapConnection {
.unwrap()
.get(&mailbox_hash)
.cloned();
debug!(&cached_uidvalidity);
debug!(&cached_max_uid);
debug!(&cached_highestmodseq);
if cached_uidvalidity.is_none()
|| cached_max_uid.is_none()
|| cached_highestmodseq.is_none()
@ -399,17 +363,11 @@ impl ImapConnection {
)
};
let mut new_unseen = BTreeSet::default();
debug!("current_uidvalidity is {}", cached_uidvalidity);
debug!("max_uid is {}", cached_max_uid);
// 1. check UIDVALIDITY. If fail, discard cache and rebuild
let select_response = self
.select_mailbox(mailbox_hash, &mut response, true)
.await?
.unwrap();
debug!(
"select_response.uidvalidity is {}",
select_response.uidvalidity
);
if select_response.uidvalidity != cached_uidvalidity {
// 1a) Check the mailbox UIDVALIDITY (see section 4.1 for more
//details) with SELECT/EXAMINE/STATUS.
@ -457,7 +415,7 @@ impl ImapConnection {
// 2. tag1 UID FETCH <lastseenuid+1>:* <descriptors>
self.send_command(
format!(
"UID FETCH {}:* (UID FLAGS ENVELOPE BODYSTRUCTURE) (CHANGEDSINCE {})",
"UID FETCH {}:* (UID FLAGS ENVELOPE BODY.PEEK[HEADER.FIELDS (REFERENCES)] BODYSTRUCTURE) (CHANGEDSINCE {})",
cached_max_uid + 1,
cached_highestmodseq,
)
@ -477,18 +435,22 @@ impl ImapConnection {
ref uid,
ref mut envelope,
ref mut flags,
ref references,
..
} in v.iter_mut()
{
let uid = uid.unwrap();
let env = envelope.as_mut().unwrap();
env.set_hash(generate_envelope_hash(&mailbox_path, &uid));
let mut tag_lck = self.uid_store.tag_index.write().unwrap();
if let Some(value) = references {
env.set_references(value);
}
let mut tag_lck = self.uid_store.collection.tag_index.write().unwrap();
if let Some((flags, keywords)) = flags {
if !flags.intersects(Flag::SEEN) {
env.set_flags(*flags);
if !env.is_seen() {
new_unseen.insert(env.hash());
}
env.set_flags(*flags);
for f in keywords {
let hash = tag_hash!(f);
if !tag_lck.contains_key(&hash) {
@ -499,14 +461,14 @@ impl ImapConnection {
}
}
{
debug!(cache_handle
cache_handle
.insert_envelopes(mailbox_hash, &v)
.chain_err_summary(|| {
format!(
"Could not save envelopes in cache for mailbox {}",
mailbox_path
)
}))?;
})?;
}
for FetchResponse { uid, envelope, .. } in v {
@ -534,14 +496,17 @@ impl ImapConnection {
payload.push((uid, env));
}
debug!("sending payload for {}", mailbox_hash);
unseen
.lock()
.unwrap()
.insert_existing_set(new_unseen.iter().cloned().collect());
mailbox_exists
.lock()
.unwrap()
.insert_existing_set(payload.iter().map(|(_, env)| env.hash()).collect::<_>());
let payload_hash_set: BTreeSet<_> =
payload.iter().map(|(_, env)| env.hash()).collect::<_>();
{
let mut unseen_lck = unseen.lock().unwrap();
for &seen_env_hash in payload_hash_set.difference(&new_unseen) {
unseen_lck.remove(seen_env_hash);
}
unseen_lck.insert_set(new_unseen);
}
mailbox_exists.lock().unwrap().insert_set(payload_hash_set);
// 3. tag2 UID FETCH 1:<lastseenuid> FLAGS
if cached_max_uid == 0 {
self.send_command(
@ -660,12 +625,11 @@ impl ImapConnection {
pub async fn init_mailbox(&mut self, mailbox_hash: MailboxHash) -> Result<SelectResponse> {
let mut response = Vec::with_capacity(8 * 1024);
let (mailbox_path, mailbox_exists, unseen, permissions) = {
let (mailbox_path, mailbox_exists, permissions) = {
let f = &self.uid_store.mailboxes.lock().await[&mailbox_hash];
(
f.imap_path().to_string(),
f.exists.clone(),
f.unseen.clone(),
f.permissions.clone(),
)
};
@ -702,14 +666,11 @@ impl ImapConnection {
permissions.set_flags = !select_response.read_only;
permissions.rename_messages = !select_response.read_only;
permissions.delete_messages = !select_response.read_only;
mailbox_exists
.lock()
.unwrap()
.set_not_yet_seen(select_response.exists);
unseen
.lock()
.unwrap()
.set_not_yet_seen(select_response.unseen);
{
let mut mailbox_exists_lck = mailbox_exists.lock().unwrap();
mailbox_exists_lck.clear();
mailbox_exists_lck.set_not_yet_seen(select_response.exists);
}
}
if select_response.exists == 0 {
return Ok(select_response);

View File

@ -63,6 +63,7 @@ pub struct ImapExtensionUse {
pub idle: bool,
#[cfg(feature = "deflate_compression")]
pub deflate: bool,
pub oauth2: bool,
}
impl Default for ImapExtensionUse {
@ -72,6 +73,7 @@ impl Default for ImapExtensionUse {
idle: true,
#[cfg(feature = "deflate_compression")]
deflate: true,
oauth2: false,
}
}
}
@ -146,26 +148,37 @@ impl ImapStream {
))
.chain_err_kind(crate::error::ErrorKind::Network)?;
if server_conf.use_starttls {
let err_fn = || {
if server_conf.server_port == 993 {
"STARTTLS failed. Server port is set to 993, which normally uses TLS. Maybe try disabling use_starttls."
} else {
"STARTTLS failed. Is the connection already encrypted?"
}
};
let mut buf = vec![0; Connection::IO_BUF_SIZE];
match server_conf.protocol {
ImapProtocol::IMAP { .. } => socket
.write_all(format!("M{} STARTTLS\r\n", cmd_id).as_bytes())
.await
.chain_err_summary(err_fn)
.chain_err_kind(crate::error::ErrorKind::Network)?,
ImapProtocol::ManageSieve => {
socket
.read(&mut buf)
.await
.chain_err_summary(err_fn)
.chain_err_kind(crate::error::ErrorKind::Network)?;
socket
.write_all(b"STARTTLS\r\n")
.await
.chain_err_summary(err_fn)
.chain_err_kind(crate::error::ErrorKind::Network)?;
}
}
socket
.flush()
.await
.chain_err_summary(err_fn)
.chain_err_kind(crate::error::ErrorKind::Network)?;
let mut response = Vec::with_capacity(1024);
let mut broken = false;
@ -175,6 +188,7 @@ impl ImapStream {
let len = socket
.read(&mut buf)
.await
.chain_err_summary(err_fn)
.chain_err_kind(crate::error::ErrorKind::Network)?;
response.extend_from_slice(&buf[0..len]);
match server_conf.protocol {
@ -200,7 +214,7 @@ impl ImapStream {
}
if !broken {
return Err(MeliError::new(format!(
"Could not initiate TLS negotiation to {}.",
"Could not initiate STARTTLS negotiation to {}.",
path
)));
}
@ -232,8 +246,13 @@ impl ImapStream {
}
}
AsyncWrapper::new(Connection::Tls(
conn_result.chain_err_kind(crate::error::ErrorKind::Network)?,
conn_result
.chain_err_summary(|| {
format!("Could not initiate TLS negotiation to {}.", path)
})
.chain_err_kind(crate::error::ErrorKind::Network)?,
))
.chain_err_summary(|| format!("Could not initiate TLS negotiation to {}.", path))
.chain_err_kind(crate::error::ErrorKind::Network)?
}
} else {
@ -334,16 +353,39 @@ impl ImapStream {
.set_err_kind(crate::error::ErrorKind::Authentication));
}
let mut capabilities = None;
ret.send_command(
format!(
"LOGIN \"{}\" \"{}\"",
&server_conf.server_username, &server_conf.server_password
)
.as_bytes(),
)
.await?;
match server_conf.protocol {
ImapProtocol::IMAP {
extension_use: ImapExtensionUse { oauth2, .. },
} if oauth2 => {
if !capabilities
.iter()
.any(|cap| cap.eq_ignore_ascii_case(b"AUTH=XOAUTH2"))
{
return Err(MeliError::new(format!(
"Could not connect to {}: OAUTH2 is enabled but server did not return AUTH=XOAUTH2 capability. Returned capabilities were: {}",
&server_conf.server_hostname,
capabilities.iter().map(|capability|
String::from_utf8_lossy(capability).to_string()).collect::<Vec<String>>().join(" ")
)));
}
ret.send_command(
format!("AUTHENTICATE XOAUTH2 {}", &server_conf.server_password).as_bytes(),
)
.await?;
}
_ => {
ret.send_command(
format!(
"LOGIN \"{}\" \"{}\"",
&server_conf.server_username, &server_conf.server_password
)
.as_bytes(),
)
.await?;
}
}
let tag_start = format!("M{} ", (ret.cmd_id - 1));
let mut capabilities = None;
loop {
ret.read_lines(&mut res, &[], false).await?;
@ -425,7 +467,6 @@ impl ImapStream {
if !termination_string.is_empty()
&& ret[last_line_idx..].starts_with(termination_string)
{
debug!(&ret[last_line_idx..]);
if !keep_termination_string {
ret.splice(last_line_idx.., std::iter::empty::<u8>());
}
@ -475,9 +516,13 @@ impl ImapStream {
self.stream.flush().await?;
match self.protocol {
ImapProtocol::IMAP { .. } => {
debug!("sent: M{} {}", self.cmd_id - 1, unsafe {
std::str::from_utf8_unchecked(command)
});
if !command.starts_with(b"LOGIN") {
debug!("sent: M{} {}", self.cmd_id - 1, unsafe {
std::str::from_utf8_unchecked(command)
});
} else {
debug!("sent: M{} LOGIN ..", self.cmd_id - 1);
}
}
ImapProtocol::ManageSieve => {}
}
@ -549,7 +594,7 @@ impl ImapConnection {
self.stream = Err(err);
}
}
if debug!(self.stream.is_ok()) {
if self.stream.is_ok() {
let mut ret = Vec::new();
if let Err(err) = try_await(async {
self.send_command(b"NOOP").await?;
@ -562,12 +607,12 @@ impl ImapConnection {
} else {
debug!(
"connect(): connection is probably alive, NOOP returned {:?}",
&ret
&String::from_utf8_lossy(&ret)
);
return Ok(());
}
}
let new_stream = debug!(ImapStream::new_connection(&self.server_conf).await);
let new_stream = ImapStream::new_connection(&self.server_conf).await;
if let Err(err) = new_stream.as_ref() {
self.uid_store.is_online.lock().unwrap().1 = Err(err.clone());
} else {
@ -583,6 +628,7 @@ impl ImapConnection {
#[cfg(feature = "deflate_compression")]
deflate,
idle: _idle,
oauth2: _,
},
} => {
if capabilities.contains(&b"CONDSTORE"[..]) && condstore {
@ -675,12 +721,16 @@ impl ImapConnection {
{
debug!(
"Received expected NO response: {:?} {:?}",
response_code, response
response_code,
String::from_utf8_lossy(&response)
);
}
ImapResponse::No(ref response_code) => {
//FIXME return error
debug!("Received NO response: {:?} {:?}", response_code, response);
debug!(
"Received NO response: {:?} {:?}",
response_code,
String::from_utf8_lossy(&response)
);
(self.uid_store.event_consumer)(
self.uid_store.account_hash,
crate::backends::BackendEvent::Notice {
@ -693,8 +743,11 @@ impl ImapConnection {
return r.into();
}
ImapResponse::Bad(ref response_code) => {
//FIXME return error
debug!("Received BAD response: {:?} {:?}", response_code, response);
debug!(
"Received BAD response: {:?} {:?}",
response_code,
String::from_utf8_lossy(&response)
);
(self.uid_store.event_consumer)(
self.uid_store.account_hash,
crate::backends::BackendEvent::Notice {
@ -987,15 +1040,10 @@ impl ImapConnection {
.await?;
self.read_response(&mut response, RequiredResponses::SEARCH)
.await?;
debug!("uid search response {:?}", &response);
let mut msn_index_lck = self.uid_store.msn_index.lock().unwrap();
let msn_index = msn_index_lck.entry(mailbox_hash).or_default();
let _ = msn_index.drain(low - 1..);
msn_index.extend(
debug!(protocol_parser::search_results(&response))?
.1
.into_iter(),
);
msn_index.extend(protocol_parser::search_results(&response)?.1.into_iter());
Ok(())
}
}
@ -1036,7 +1084,6 @@ impl ImapBlockingConnection {
let mut prev_failure = None;
async move {
if self.conn.stream.is_err() {
debug!(&self.conn.stream);
return None;
}
loop {
@ -1070,7 +1117,6 @@ async fn read(
}
Ok(b) => {
result.extend_from_slice(&buf[0..b]);
debug!(unsafe { std::str::from_utf8_unchecked(result) });
if let Some(pos) = result.find(b"\r\n") {
*prev_res_length = pos + b"\r\n".len();
return Some(result[0..*prev_res_length].to_vec());
@ -1078,8 +1124,6 @@ async fn read(
*prev_failure = None;
}
Err(_err) => {
debug!(&conn.stream);
debug!(&_err);
*err = Some(Into::<MeliError>::into(_err).set_kind(crate::error::ErrorKind::Network));
*break_flag = true;
*prev_failure = Some(SystemTime::now());

View File

@ -21,80 +21,11 @@
use super::protocol_parser::SelectResponse;
use crate::backends::{
BackendMailbox, Mailbox, MailboxHash, MailboxPermissions, SpecialUsageMailbox,
BackendMailbox, LazyCountSet, Mailbox, MailboxHash, MailboxPermissions, SpecialUsageMailbox,
};
use crate::email::EnvelopeHash;
use crate::error::*;
use std::collections::BTreeSet;
use std::sync::{Arc, Mutex, RwLock};
#[derive(Debug, Default, Clone)]
pub struct LazyCountSet {
not_yet_seen: usize,
set: BTreeSet<EnvelopeHash>,
}
impl LazyCountSet {
pub fn set_not_yet_seen(&mut self, new_val: usize) {
self.not_yet_seen = new_val;
}
pub fn insert_existing(&mut self, new_val: EnvelopeHash) -> bool {
if self.not_yet_seen == 0 {
false
} else {
self.not_yet_seen -= 1;
self.set.insert(new_val);
true
}
}
pub fn insert_existing_set(&mut self, set: BTreeSet<EnvelopeHash>) -> bool {
debug!("insert_existing_set {:?}", &set);
if self.not_yet_seen < set.len() {
false
} else {
self.not_yet_seen -= set.len();
self.set.extend(set.into_iter());
true
}
}
#[inline(always)]
pub fn len(&self) -> usize {
self.set.len() + self.not_yet_seen
}
#[inline(always)]
pub fn clear(&mut self) {
self.set.clear();
self.not_yet_seen = 0;
}
pub fn insert_new(&mut self, new_val: EnvelopeHash) {
self.set.insert(new_val);
}
pub fn insert_set(&mut self, set: BTreeSet<EnvelopeHash>) {
debug!("insert__set {:?}", &set);
self.set.extend(set.into_iter());
}
pub fn remove(&mut self, new_val: EnvelopeHash) -> bool {
self.set.remove(&new_val)
}
}
#[test]
fn test_lazy_count_set() {
let mut new = LazyCountSet::default();
new.set_not_yet_seen(10);
for i in 0..10 {
assert!(new.insert_existing(i));
}
assert!(!new.insert_existing(10));
}
#[derive(Debug, Default, Clone)]
pub struct ImapMailbox {
pub hash: MailboxHash,
@ -112,12 +43,31 @@ pub struct ImapMailbox {
pub permissions: Arc<Mutex<MailboxPermissions>>,
pub exists: Arc<Mutex<LazyCountSet>>,
pub unseen: Arc<Mutex<LazyCountSet>>,
pub warm: Arc<Mutex<bool>>,
}
impl ImapMailbox {
pub fn imap_path(&self) -> &str {
&self.imap_path
}
/// Establish that mailbox contents have been fetched at least once during this execution
#[inline(always)]
pub fn set_warm(&self, new_value: bool) {
*self.warm.lock().unwrap() = new_value;
}
/// Mailbox contents have been fetched at least once during this execution
#[inline(always)]
pub fn is_warm(&self) -> bool {
*self.warm.lock().unwrap()
}
/// Mailbox contents have not been fetched at all during this execution
#[inline(always)]
pub fn is_cold(&self) -> bool {
!self.is_warm()
}
}
impl BackendMailbox for ImapMailbox {

View File

@ -103,12 +103,11 @@ impl BackendOp for ImapOp {
//flags.lock().await.set(Some(_flags));
cache.flags = Some(_flags);
}
cache.bytes =
Some(unsafe { std::str::from_utf8_unchecked(body.unwrap()).to_string() });
cache.bytes = Some(body.unwrap().to_vec());
}
let mut bytes_cache = uid_store.byte_cache.lock()?;
let cache = bytes_cache.entry(uid).or_default();
let ret = cache.bytes.clone().unwrap().into_bytes();
let ret = cache.bytes.clone().unwrap();
Ok(ret)
}))
}
@ -145,9 +144,9 @@ impl BackendOp for ImapOp {
.map_err(MeliError::from)?;
if v.len() != 1 {
debug!("responses len is {}", v.len());
debug!(&response);
debug!(String::from_utf8_lossy(&response));
/* TODO: Trigger cache invalidation here. */
debug!(format!("message with UID {} was not found", uid));
debug!("message with UID {} was not found", uid);
return Err(MeliError::new(format!(
"Invalid/unexpected response: {:?}",
response

View File

@ -314,46 +314,30 @@ fn test_imap_response() {
assert_eq!(ImapResponse::try_from(&b"M12 NO [CANNOT] Invalid mailbox name: Name must not have \'/\' characters (0.000 + 0.098 + 0.097 secs).\r\n"[..]).unwrap(), ImapResponse::No(ResponseCode::Alert("Invalid mailbox name: Name must not have '/' characters".to_string())));
}
impl<'a> std::iter::DoubleEndedIterator for ImapLineIterator<'a> {
fn next_back(&mut self) -> Option<Self::Item> {
if self.slice.is_empty() {
None
} else if let Some(pos) = self.slice.rfind(b"\r\n") {
if self.slice.get(..pos).unwrap_or_default().is_empty() {
self.slice = self.slice.get(..pos).unwrap_or_default();
None
} else if let Some(prev_pos) = self.slice.get(..pos).unwrap_or_default().rfind(b"\r\n")
{
let ret = self.slice.get(prev_pos + 2..pos + 2).unwrap_or_default();
self.slice = self.slice.get(..prev_pos + 2).unwrap_or_default();
Some(ret)
} else {
let ret = self.slice;
self.slice = self.slice.get(ret.len()..).unwrap_or_default();
Some(ret)
}
} else {
let ret = self.slice;
self.slice = self.slice.get(ret.len()..).unwrap_or_default();
Some(ret)
}
}
}
impl<'a> Iterator for ImapLineIterator<'a> {
type Item = &'a [u8];
fn next(&mut self) -> Option<&'a [u8]> {
if self.slice.is_empty() {
None
} else if let Some(pos) = self.slice.find(b"\r\n") {
let ret = self.slice.get(..pos + 2).unwrap_or_default();
self.slice = self.slice.get(pos + 2..).unwrap_or_default();
Some(ret)
} else {
let ret = self.slice;
self.slice = self.slice.get(ret.len()..).unwrap_or_default();
Some(ret)
return None;
}
let mut i = 0;
loop {
let cur_slice = &self.slice[i..];
if let Some(pos) = cur_slice.find(b"\r\n") {
/* Skip literal continuation line */
if cur_slice.get(pos.saturating_sub(1)) == Some(&b'}') {
i += pos + 2;
continue;
}
let ret = self.slice.get(..i + pos + 2).unwrap_or_default();
self.slice = self.slice.get(i + pos + 2..).unwrap_or_default();
return Some(ret);
} else {
let ret = self.slice;
self.slice = self.slice.get(ret.len()..).unwrap_or_default();
return Some(ret);
}
}
}
}
@ -372,6 +356,57 @@ macro_rules! to_str (
($v:expr) => (unsafe{ std::str::from_utf8_unchecked($v) })
);
#[test]
fn test_imap_line_iterator() {
{
let s = b"* 1429 FETCH (UID 1505 FLAGS (\\Seen) RFC822 {26}\r\nReturn-Path: <blah blah...\r\n* 1430 FETCH (UID 1506 FLAGS (\\Seen)\r\n* 1431 FETCH (UID 1507 FLAGS (\\Seen)\r\n* 1432 FETCH (UID 1500 FLAGS (\\Seen) RFC822 {4}\r\nnull\r\n";
let line_a =
b"* 1429 FETCH (UID 1505 FLAGS (\\Seen) RFC822 {26}\r\nReturn-Path: <blah blah...\r\n";
let line_b = b"* 1430 FETCH (UID 1506 FLAGS (\\Seen)\r\n";
let line_c = b"* 1431 FETCH (UID 1507 FLAGS (\\Seen)\r\n";
let line_d = b"* 1432 FETCH (UID 1500 FLAGS (\\Seen) RFC822 {4}\r\nnull\r\n";
let mut iter = s.split_rn();
assert_eq!(to_str!(iter.next().unwrap()), to_str!(line_a));
assert_eq!(to_str!(iter.next().unwrap()), to_str!(line_b));
assert_eq!(to_str!(iter.next().unwrap()), to_str!(line_c));
assert_eq!(to_str!(iter.next().unwrap()), to_str!(line_d));
assert!(iter.next().is_none());
}
{
let s = b"* 23 FETCH (FLAGS (\\Seen) RFC822.SIZE 44827)\r\n";
let mut iter = s.split_rn();
assert_eq!(to_str!(iter.next().unwrap()), to_str!(s));
assert!(iter.next().is_none());
}
{
let s = b"";
let mut iter = s.split_rn();
assert!(iter.next().is_none());
}
{
let s = b"* 172 EXISTS\r\n* 1 RECENT\r\n* OK [UNSEEN 12] Message 12 is first unseen\r\n* OK [UIDVALIDITY 3857529045] UIDs valid\r\n* OK [UIDNEXT 4392] Predicted next UID\r\n* FLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft)\r\n* OK [PERMANENTFLAGS (\\Deleted \\Seen \\*)] Limited\r\n* OK [NOMODSEQ] Sorry, this mailbox format doesn't support modsequences\r\n* A142 OK [READ-WRITE] SELECT completed\r\n";
let mut iter = s.split_rn();
for l in &[
&b"* 172 EXISTS\r\n"[..],
&b"* 1 RECENT\r\n"[..],
&b"* OK [UNSEEN 12] Message 12 is first unseen\r\n"[..],
&b"* OK [UIDVALIDITY 3857529045] UIDs valid\r\n"[..],
&b"* OK [UIDNEXT 4392] Predicted next UID\r\n"[..],
&b"* FLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft)\r\n"[..],
&b"* OK [PERMANENTFLAGS (\\Deleted \\Seen \\*)] Limited\r\n"[..],
&b"* OK [NOMODSEQ] Sorry, this mailbox format doesn't support modsequences\r\n"[..],
&b"* A142 OK [READ-WRITE] SELECT completed\r\n"[..],
] {
assert_eq!(to_str!(iter.next().unwrap()), to_str!(l));
}
assert!(iter.next().is_none());
}
}
/*macro_rules! dbg_dmp (
($i: expr, $submac:ident!( $($args:tt)* )) => (
{
@ -415,7 +450,13 @@ pub fn list_mailbox_result(input: &[u8]) -> IResult<&[u8], ImapMailbox> {
let separator: u8 = separator[0];
let mut f = ImapMailbox::default();
f.no_select = false;
f.is_subscribed = path.eq_ignore_ascii_case("INBOX");
f.is_subscribed = false;
if path.eq_ignore_ascii_case("INBOX") {
f.is_subscribed = true;
let _ = f.set_special_usage(SpecialUsageMailbox::Inbox);
}
for p in properties.split(|&b| b == b' ') {
if p.eq_ignore_ascii_case(b"\\NoSelect") || p.eq_ignore_ascii_case(b"\\NonExistent")
{
@ -425,9 +466,15 @@ pub fn list_mailbox_result(input: &[u8]) -> IResult<&[u8], ImapMailbox> {
} else if p.eq_ignore_ascii_case(b"\\Sent") {
let _ = f.set_special_usage(SpecialUsageMailbox::Sent);
} else if p.eq_ignore_ascii_case(b"\\Junk") {
let _ = f.set_special_usage(SpecialUsageMailbox::Junk);
} else if p.eq_ignore_ascii_case(b"\\Trash") {
let _ = f.set_special_usage(SpecialUsageMailbox::Trash);
} else if p.eq_ignore_ascii_case(b"\\Drafts") {
let _ = f.set_special_usage(SpecialUsageMailbox::Drafts);
} else if p.eq_ignore_ascii_case(b"\\Flagged") {
let _ = f.set_special_usage(SpecialUsageMailbox::Flagged);
} else if p.eq_ignore_ascii_case(b"\\Archive") {
let _ = f.set_special_usage(SpecialUsageMailbox::Archive);
}
}
f.imap_path = path.to_string();
@ -458,7 +505,9 @@ pub struct FetchResponse<'a> {
pub modseq: Option<ModSequence>,
pub flags: Option<(Flag, Vec<String>)>,
pub body: Option<&'a [u8]>,
pub references: Option<&'a [u8]>,
pub envelope: Option<Envelope>,
pub raw_fetch_value: &'a [u8],
}
pub fn fetch_response(input: &[u8]) -> ImapParseResult<FetchResponse<'_>> {
@ -513,7 +562,9 @@ pub fn fetch_response(input: &[u8]) -> ImapParseResult<FetchResponse<'_>> {
modseq: None,
flags: None,
body: None,
references: None,
envelope: None,
raw_fetch_value: &[],
};
while input[i].is_ascii_digit() {
@ -609,6 +660,22 @@ pub fn fetch_response(input: &[u8]) -> ImapParseResult<FetchResponse<'_>> {
let (rest, _has_attachments) = bodystructure_has_attachments(&input[i..])?;
has_attachments = _has_attachments;
i += input[i..].len() - rest.len();
} else if input[i..].starts_with(b"BODY[HEADER.FIELDS (REFERENCES)] ") {
i += b"BODY[HEADER.FIELDS (REFERENCES)] ".len();
if let Ok((rest, mut references)) = astring_token(&input[i..]) {
if !references.trim().is_empty() {
if let Ok((_, (_, v))) = crate::email::parser::headers::header(&references) {
references = v;
}
ret.references = Some(references);
}
i += input.len() - i - rest.len();
} else {
return debug!(Err(MeliError::new(format!(
"Unexpected input while parsing UID FETCH response. Got: `{:.40}`",
String::from_utf8_lossy(&input[i..])
))));
}
} else if input[i..].starts_with(b")\r\n") {
i += b")\r\n".len();
break;
@ -623,6 +690,7 @@ pub fn fetch_response(input: &[u8]) -> ImapParseResult<FetchResponse<'_>> {
))));
}
}
ret.raw_fetch_value = &input[..i];
if let Some(env) = ret.envelope.as_mut() {
env.set_has_attachments(has_attachments);
@ -823,7 +891,10 @@ pub fn untagged_responses(input: &[u8]) -> ImapParseResult<Option<UntaggedRespon
let (input, _tag) =
take_until::<_, &[u8], (&[u8], nom::error::ErrorKind)>(&b"\r\n"[..])(input)?;
let (input, _) = tag::<_, &[u8], (&[u8], nom::error::ErrorKind)>(b"\r\n")(input)?;
debug!("Parse untagged response from {:?}", orig_input);
debug!(
"Parse untagged response from {:?}",
String::from_utf8_lossy(&orig_input)
);
Ok((
input,
{
@ -848,7 +919,6 @@ pub fn untagged_responses(input: &[u8]) -> ImapParseResult<Option<UntaggedRespon
#[test]
fn test_untagged_responses() {
use std::convert::TryInto;
use UntaggedResponse::*;
assert_eq!(
untagged_responses(b"* 2 EXISTS\r\n")
@ -868,7 +938,9 @@ fn test_untagged_responses() {
modseq: Some(ModSequence(std::num::NonZeroU64::new(1365_u64).unwrap())),
flags: Some((Flag::SEEN, vec![])),
body: None,
envelope: None
references: None,
envelope: None,
raw_fetch_value: &b"* 1079 FETCH (UID 1103 MODSEQ (1365) FLAGS (\\Seen))\r\n"[..],
})
);
assert_eq!(
@ -882,7 +954,9 @@ fn test_untagged_responses() {
modseq: None,
flags: Some((Flag::SEEN, vec![])),
body: None,
envelope: None
references: None,
envelope: None,
raw_fetch_value: &b"* 1 FETCH (FLAGS (\\Seen))\r\n"[..],
})
);
}
@ -944,7 +1018,7 @@ pub struct SelectResponse {
pub exists: ImapNum,
pub recent: ImapNum,
pub flags: (Flag, Vec<String>),
pub unseen: MessageSequenceNumber,
pub first_unseen: MessageSequenceNumber,
pub uidvalidity: UIDVALIDITY,
pub uidnext: UID,
pub permanentflags: (Flag, Vec<String>),
@ -991,7 +1065,7 @@ pub fn select_response(input: &[u8]) -> Result<SelectResponse> {
} else if l.starts_with(b"* FLAGS (") {
ret.flags = flags(&l[b"* FLAGS (".len()..l.len() - b")".len()]).map(|(_, v)| v)?;
} else if l.starts_with(b"* OK [UNSEEN ") {
ret.unseen = MessageSequenceNumber::from_str(&String::from_utf8_lossy(
ret.first_unseen = MessageSequenceNumber::from_str(&String::from_utf8_lossy(
&l[b"* OK [UNSEEN ".len()..l.find(b"]").unwrap()],
))?;
} else if l.starts_with(b"* OK [UIDVALIDITY ") {
@ -1038,7 +1112,6 @@ pub fn select_response(input: &[u8]) -> Result<SelectResponse> {
#[test]
fn test_select_response() {
use std::convert::TryInto;
let r = b"* FLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft)\r\n* OK [PERMANENTFLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft \\*)] Flags permitted.\r\n* 45 EXISTS\r\n* 0 RECENT\r\n* OK [UNSEEN 16] First unseen.\r\n* OK [UIDVALIDITY 1554422056] UIDs valid\r\n* OK [UIDNEXT 50] Predicted next UID\r\n";
assert_eq!(
@ -1050,7 +1123,7 @@ fn test_select_response() {
Flag::REPLIED | Flag::SEEN | Flag::TRASHED | Flag::DRAFT | Flag::FLAGGED,
Vec::new()
),
unseen: 16,
first_unseen: 16,
uidvalidity: 1554422056,
uidnext: 50,
permanentflags: (
@ -1073,7 +1146,7 @@ fn test_select_response() {
Flag::REPLIED | Flag::SEEN | Flag::TRASHED | Flag::DRAFT | Flag::FLAGGED,
Vec::new()
),
unseen: 12,
first_unseen: 12,
uidvalidity: 3857529045,
uidnext: 4392,
permanentflags: (Flag::SEEN | Flag::TRASHED, vec!["*".into()]),
@ -1095,7 +1168,7 @@ fn test_select_response() {
Flag::REPLIED | Flag::SEEN | Flag::TRASHED | Flag::DRAFT | Flag::FLAGGED,
Vec::new()
),
unseen: 12,
first_unseen: 12,
uidvalidity: 3857529045,
uidnext: 4392,
permanentflags: (Flag::SEEN | Flag::TRASHED, vec!["*".into()]),
@ -1112,7 +1185,8 @@ pub fn flags(input: &[u8]) -> IResult<&[u8], (Flag, Vec<String>)> {
let mut input = input;
while !input.starts_with(b")") && !input.is_empty() {
if input.starts_with(b"\\") {
let is_system_flag = input.starts_with(b"\\");
if is_system_flag {
input = &input[1..];
}
let mut match_end = 0;
@ -1123,23 +1197,24 @@ pub fn flags(input: &[u8]) -> IResult<&[u8], (Flag, Vec<String>)> {
match_end += 1;
}
match &input[..match_end] {
b"Answered" => {
match (is_system_flag, &input[..match_end]) {
(true, t) if t.eq_ignore_ascii_case(b"Answered") => {
ret.set(Flag::REPLIED, true);
}
b"Flagged" => {
(true, t) if t.eq_ignore_ascii_case(b"Flagged") => {
ret.set(Flag::FLAGGED, true);
}
b"Deleted" => {
(true, t) if t.eq_ignore_ascii_case(b"Deleted") => {
ret.set(Flag::TRASHED, true);
}
b"Seen" => {
(true, t) if t.eq_ignore_ascii_case(b"Seen") => {
ret.set(Flag::SEEN, true);
}
b"Draft" => {
(true, t) if t.eq_ignore_ascii_case(b"Draft") => {
ret.set(Flag::DRAFT, true);
}
f => {
(true, t) if t.eq_ignore_ascii_case(b"Recent") => { /* ignore */ }
(_, f) => {
keywords.push(String::from_utf8_lossy(&f).into());
}
}
@ -1243,7 +1318,9 @@ pub fn envelope(input: &[u8]) -> IResult<&[u8], Envelope> {
}
if let Some(in_reply_to) = in_reply_to {
env.set_in_reply_to(&in_reply_to);
env.push_references(env.in_reply_to().unwrap().clone());
if let Some(in_reply_to) = env.in_reply_to().cloned() {
env.push_references(in_reply_to);
}
}
if let Some(message_id) = message_id {
@ -1420,7 +1497,7 @@ pub fn bodystructure_has_attachments(input: &[u8]) -> IResult<&[u8], bool> {
let mut has_attachments = false;
let mut first_in_line = true;
while !input.is_empty() && !input.starts_with(b")") {
if input.starts_with(b"\"") || input[0].is_ascii_alphanumeric() {
if input.starts_with(b"\"") || input[0].is_ascii_alphanumeric() || input[0] == b'{' {
let (_input, token) = astring_token(input)?;
input = _input;
if first_in_line {

View File

@ -28,14 +28,13 @@ use crate::backends::{
RefreshEvent,
RefreshEventKind::{self, *},
};
use crate::email::Envelope;
use crate::error::*;
use std::convert::TryInto;
impl ImapConnection {
pub async fn process_untagged(&mut self, line: &[u8]) -> Result<bool> {
macro_rules! try_fail {
($mailbox_hash: expr, $($result:expr)+) => {
($mailbox_hash: expr, $($result:expr $(,)*)+) => {
$(if let Err(err) = $result {
self.uid_store.is_online.lock().unwrap().1 = Err(err.clone());
debug!("failure: {}", err.to_string());
@ -78,7 +77,7 @@ impl ImapConnection {
.lock()
.unwrap()
.get(&mailbox_hash)
.map(|i| i.len() < n.try_into().unwrap())
.map(|i| i.len() < TryInto::<usize>::try_into(n).unwrap())
.unwrap_or(true)
{
debug!(
@ -86,6 +85,64 @@ impl ImapConnection {
n,
self.uid_store.msn_index.lock().unwrap().get(&mailbox_hash)
);
self.send_command("UID SEARCH 1:*".as_bytes()).await?;
self.read_response(&mut response, RequiredResponses::SEARCH)
.await?;
let results = super::protocol_parser::search_results(&response)?
.1
.into_iter()
.collect::<std::collections::BTreeSet<UID>>();
{
let mut lck = self.uid_store.msn_index.lock().unwrap();
let msn_index = lck.entry(mailbox_hash).or_default();
msn_index.clear();
msn_index.extend(
super::protocol_parser::search_results(&response)?
.1
.into_iter(),
);
}
let mut events = vec![];
for (deleted_uid, deleted_hash) in self
.uid_store
.uid_index
.lock()
.unwrap()
.iter()
.filter(|((mailbox_hash_, u), _)| {
*mailbox_hash_ == mailbox_hash && !results.contains(u)
})
.map(|((_, uid), hash)| (*uid, *hash))
.collect::<Vec<(UID, crate::email::EnvelopeHash)>>()
{
mailbox.exists.lock().unwrap().remove(deleted_hash);
mailbox.unseen.lock().unwrap().remove(deleted_hash);
self.uid_store
.uid_index
.lock()
.unwrap()
.remove(&(mailbox_hash, deleted_uid));
self.uid_store
.hash_index
.lock()
.unwrap()
.remove(&deleted_hash);
events.push((
deleted_uid,
RefreshEvent {
account_hash: self.uid_store.account_hash,
mailbox_hash,
kind: Remove(deleted_hash),
},
));
}
if self.uid_store.keep_offline_cache {
cache_handle.update(mailbox_hash, &events)?;
}
for (_, event) in events {
self.add_refresh_event(event);
}
return Ok(true);
}
let deleted_uid = self
@ -95,7 +152,7 @@ impl ImapConnection {
.unwrap()
.entry(mailbox_hash)
.or_default()
.remove(n.try_into().unwrap());
.remove(TryInto::<usize>::try_into(n).unwrap().saturating_sub(1));
debug!("expunge {}, UID = {}", n, deleted_uid);
let deleted_hash: crate::email::EnvelopeHash = match self
.uid_store
@ -107,6 +164,8 @@ impl ImapConnection {
Some(v) => v,
None => return Ok(true),
};
mailbox.exists.lock().unwrap().remove(deleted_hash);
mailbox.unseen.lock().unwrap().remove(deleted_hash);
self.uid_store
.hash_index
.lock()
@ -136,94 +195,108 @@ impl ImapConnection {
debug!("exists {}", n);
try_fail!(
mailbox_hash,
self.send_command(format!("FETCH {} (UID FLAGS RFC822)", n).as_bytes()).await
self.send_command(format!("FETCH {} (UID FLAGS ENVELOPE BODY.PEEK[HEADER.FIELDS (REFERENCES)] BODYSTRUCTURE)", n).as_bytes()).await
self.read_response(&mut response, RequiredResponses::FETCH_REQUIRED).await
);
match super::protocol_parser::fetch_responses(&response) {
Ok((_, v, _)) => {
'fetch_responses: for FetchResponse {
uid, flags, body, ..
} in v
{
if uid.is_none() || flags.is_none() || body.is_none() {
continue;
}
let uid = uid.unwrap();
if self
.uid_store
.uid_index
.lock()
.unwrap()
.contains_key(&(mailbox_hash, uid))
{
continue 'fetch_responses;
}
let env_hash = generate_envelope_hash(&mailbox.imap_path(), &uid);
self.uid_store
.msn_index
.lock()
.unwrap()
.entry(mailbox_hash)
.or_default()
.push(uid);
if let Ok(mut env) =
Envelope::from_bytes(body.unwrap(), flags.as_ref().map(|&(f, _)| f))
{
env.set_hash(env_hash);
self.uid_store
.hash_index
.lock()
.unwrap()
.insert(env_hash, (uid, mailbox_hash));
self.uid_store
.uid_index
.lock()
.unwrap()
.insert((mailbox_hash, uid), env_hash);
if let Some((_, keywords)) = flags {
let mut tag_lck = self.uid_store.tag_index.write().unwrap();
for f in keywords {
let hash = tag_hash!(f);
if !tag_lck.contains_key(&hash) {
tag_lck.insert(hash, f);
}
env.labels_mut().push(hash);
}
}
debug!(
"Create event {} {} {}",
env.hash(),
env.subject(),
mailbox.path(),
);
if !env.is_seen() {
mailbox.unseen.lock().unwrap().insert_new(env.hash());
}
mailbox.exists.lock().unwrap().insert_new(env.hash());
let mut event: [(UID, RefreshEvent); 1] = [(
uid,
RefreshEvent {
account_hash: self.uid_store.account_hash,
mailbox_hash,
kind: Create(Box::new(env)),
},
)];
if self.uid_store.keep_offline_cache {
cache_handle.update(mailbox_hash, &event)?;
}
self.add_refresh_event(std::mem::replace(
&mut event[0].1,
RefreshEvent {
account_hash: self.uid_store.account_hash,
mailbox_hash,
kind: Rescan,
},
));
let mut v = match super::protocol_parser::fetch_responses(&response) {
Ok((_, v, _)) => v,
Err(err) => {
debug!(
"Error when parsing FETCH response after untagged exists {:?}",
err
);
return Ok(true);
}
};
debug!("responses len is {}", v.len());
for FetchResponse {
ref uid,
ref mut envelope,
ref mut flags,
ref references,
..
} in &mut v
{
if uid.is_none() || flags.is_none() || envelope.is_none() {
continue;
}
let uid = uid.unwrap();
let env = envelope.as_mut().unwrap();
env.set_hash(generate_envelope_hash(&mailbox.imap_path(), &uid));
if let Some(value) = references {
env.set_references(value);
}
let mut tag_lck = self.uid_store.collection.tag_index.write().unwrap();
if let Some((flags, keywords)) = flags {
env.set_flags(*flags);
if !env.is_seen() {
mailbox.unseen.lock().unwrap().insert_new(env.hash());
}
for f in keywords {
let hash = tag_hash!(f);
if !tag_lck.contains_key(&hash) {
tag_lck.insert(hash, f.to_string());
}
env.labels_mut().push(hash);
}
}
Err(e) => {
debug!(e);
mailbox.exists.lock().unwrap().insert_new(env.hash());
if !self
.uid_store
.uid_index
.lock()
.unwrap()
.contains_key(&(mailbox_hash, uid))
{
self.uid_store
.msn_index
.lock()
.unwrap()
.entry(mailbox_hash)
.or_default()
.push(uid);
}
self.uid_store
.hash_index
.lock()
.unwrap()
.insert(env.hash(), (uid, mailbox_hash));
self.uid_store
.uid_index
.lock()
.unwrap()
.insert((mailbox_hash, uid), env.hash());
debug!(
"Create event {} {} {}",
env.hash(),
env.subject(),
mailbox.path(),
);
}
if self.uid_store.keep_offline_cache {
if let Err(err) = cache_handle
.insert_envelopes(mailbox_hash, &v)
.chain_err_summary(|| {
format!(
"Could not save envelopes in cache for mailbox {}",
&mailbox.imap_path()
)
})
{
crate::log(err.to_string(), crate::INFO);
}
}
for response in v {
if let FetchResponse {
envelope: Some(envelope),
..
} = response
{
self.add_refresh_event(RefreshEvent {
account_hash: self.uid_store.account_hash,
mailbox_hash,
kind: Create(Box::new(envelope)),
});
}
}
}
@ -241,97 +314,120 @@ impl ImapConnection {
debug!("UID SEARCH RECENT returned no results");
}
Ok(v) => {
let command = {
let mut iter = v.split(u8::is_ascii_whitespace);
let first = iter.next().unwrap_or(v);
let mut accum = format!("{}", to_str!(first).trim());
for ms in iter {
accum = format!("{},{}", accum, to_str!(ms).trim());
}
format!("UID FETCH {} (UID FLAGS ENVELOPE BODY.PEEK[HEADER.FIELDS (REFERENCES)] BODYSTRUCTURE)", accum)
};
try_fail!(
mailbox_hash,
self.send_command(
&[b"UID FETCH", v, b"(FLAGS RFC822)"]
.join(&b' '),
).await
self.send_command(command.as_bytes()).await
self.read_response(&mut response, RequiredResponses::FETCH_REQUIRED).await
);
debug!(&response);
match super::protocol_parser::fetch_responses(&response) {
Ok((_, v, _)) => {
for FetchResponse {
uid, flags, body, ..
} in v
{
if uid.is_none() || flags.is_none() || body.is_none() {
continue;
}
let uid = uid.unwrap();
if !self
.uid_store
.uid_index
.lock()
.unwrap()
.contains_key(&(mailbox_hash, uid))
{
if let Ok(mut env) = Envelope::from_bytes(
body.unwrap(),
flags.as_ref().map(|&(f, _)| f),
) {
self.uid_store
.hash_index
.lock()
.unwrap()
.insert(env.hash(), (uid, mailbox_hash));
self.uid_store
.uid_index
.lock()
.unwrap()
.insert((mailbox_hash, uid), env.hash());
debug!(
"Create event {} {} {}",
env.hash(),
env.subject(),
mailbox.path(),
);
if let Some((_, keywords)) = flags {
let mut tag_lck =
self.uid_store.tag_index.write().unwrap();
for f in keywords {
let hash = tag_hash!(f);
if !tag_lck.contains_key(&hash) {
tag_lck.insert(hash, f);
}
env.labels_mut().push(hash);
}
}
if !env.is_seen() {
mailbox
.unseen
.lock()
.unwrap()
.insert_new(env.hash());
}
mailbox.exists.lock().unwrap().insert_new(env.hash());
let mut event: [(UID, RefreshEvent); 1] = [(
uid,
RefreshEvent {
account_hash: self.uid_store.account_hash,
mailbox_hash,
kind: Create(Box::new(env)),
},
)];
if self.uid_store.keep_offline_cache {
cache_handle.update(mailbox_hash, &event)?;
}
self.add_refresh_event(std::mem::replace(
&mut event[0].1,
RefreshEvent {
account_hash: self.uid_store.account_hash,
mailbox_hash,
kind: Rescan,
},
));
}
let mut v = match super::protocol_parser::fetch_responses(&response) {
Ok((_, v, _)) => v,
Err(err) => {
debug!(
"Error when parsing FETCH response after untagged recent {:?}",
err
);
return Ok(true);
}
};
debug!("responses len is {}", v.len());
for FetchResponse {
ref uid,
ref mut envelope,
ref mut flags,
ref references,
..
} in &mut v
{
if uid.is_none() || flags.is_none() || envelope.is_none() {
continue;
}
let uid = uid.unwrap();
let env = envelope.as_mut().unwrap();
env.set_hash(generate_envelope_hash(&mailbox.imap_path(), &uid));
if let Some(value) = references {
env.set_references(value);
}
let mut tag_lck = self.uid_store.collection.tag_index.write().unwrap();
if let Some((flags, keywords)) = flags {
env.set_flags(*flags);
if !env.is_seen() {
mailbox.unseen.lock().unwrap().insert_new(env.hash());
}
for f in keywords {
let hash = tag_hash!(f);
if !tag_lck.contains_key(&hash) {
tag_lck.insert(hash, f.to_string());
}
env.labels_mut().push(hash);
}
}
Err(e) => {
debug!(e);
mailbox.exists.lock().unwrap().insert_new(env.hash());
}
if self.uid_store.keep_offline_cache {
if let Err(err) = cache_handle
.insert_envelopes(mailbox_hash, &v)
.chain_err_summary(|| {
format!(
"Could not save envelopes in cache for mailbox {}",
&mailbox.imap_path()
)
})
{
crate::log(err.to_string(), crate::INFO);
}
}
for response in v {
if let FetchResponse {
envelope: Some(envelope),
uid: Some(uid),
..
} = response
{
if !self
.uid_store
.uid_index
.lock()
.unwrap()
.contains_key(&(mailbox_hash, uid))
{
self.uid_store
.msn_index
.lock()
.unwrap()
.entry(mailbox_hash)
.or_default()
.push(uid);
}
self.uid_store
.hash_index
.lock()
.unwrap()
.insert(envelope.hash(), (uid, mailbox_hash));
self.uid_store
.uid_index
.lock()
.unwrap()
.insert((mailbox_hash, uid), envelope.hash());
debug!(
"Create event {} {} {}",
envelope.hash(),
envelope.subject(),
mailbox.path(),
);
self.add_refresh_event(RefreshEvent {
account_hash: self.uid_store.account_hash,
mailbox_hash,
kind: Create(Box::new(envelope)),
});
}
}
}
@ -350,38 +446,21 @@ impl ImapConnection {
modseq,
flags,
body: _,
references: _,
envelope: _,
raw_fetch_value: _,
}) => {
if let Some(modseq) = modseq {
if self
.uid_store
.reverse_modseq
.lock()
.unwrap()
.entry(mailbox_hash)
.or_default()
.contains_key(&modseq)
{
return Ok(true);
}
}
if let Some(flags) = flags {
let uid = if let Some(uid) = uid {
uid
} else {
try_fail!(
mailbox_hash,
self.send_command(
&[
b"UID SEARCH",
format!("{}", msg_seq).as_bytes(),
]
.join(&b' '),
).await
self.read_response(&mut response, RequiredResponses::SEARCH).await
);
debug!(to_str!(&response));
mailbox_hash,
self.send_command(format!("UID SEARCH {}", msg_seq).as_bytes())
.await,
self.read_response(&mut response, RequiredResponses::SEARCH)
.await,
);
match super::protocol_parser::search_results(
response.split_rn().next().unwrap_or(b""),
)
@ -392,34 +471,30 @@ impl ImapConnection {
return Ok(false);
}
Err(e) => {
debug!("SEARCH error failed: {}", e);
debug!(to_str!(&response));
debug!(e);
return Ok(false);
}
}
};
debug!("fetch uid {} {:?}", uid, flags);
let env_hash = self
.uid_store
.uid_index
.lock()
.unwrap()
.get(&(mailbox_hash, uid))
.copied();
if let Some(env_hash) = env_hash {
if let Some(env_hash) = {
let temp = self
.uid_store
.uid_index
.lock()
.unwrap()
.get(&(mailbox_hash, uid))
.copied();
temp
} {
if !flags.0.intersects(crate::email::Flag::SEEN) {
mailbox.unseen.lock().unwrap().insert_new(env_hash);
} else {
mailbox.unseen.lock().unwrap().remove(env_hash);
}
mailbox.exists.lock().unwrap().insert_new(env_hash);
if let Some(modseq) = modseq {
self.uid_store
.reverse_modseq
.lock()
.unwrap()
.entry(mailbox_hash)
.or_default()
.insert(modseq, env_hash);
self.uid_store
.modseq
.lock()

View File

@ -76,10 +76,9 @@ pub async fn idle(kit: ImapWatchKit) -> Result<()> {
let mailbox_hash = mailbox.hash();
let mut response = Vec::with_capacity(8 * 1024);
let select_response = conn
.select_mailbox(mailbox_hash, &mut response, true)
.examine_mailbox(mailbox_hash, &mut response, true)
.await?
.unwrap();
debug!("select response {}", String::from_utf8_lossy(&response));
{
let mut uidvalidities = uid_store.uidvalidity.lock().unwrap();
@ -111,6 +110,12 @@ pub async fn idle(kit: ImapWatchKit) -> Result<()> {
let mailboxes_lck = timeout(uid_store.timeout, uid_store.mailboxes.lock()).await?;
mailboxes_lck.clone()
};
for (h, mailbox) in mailboxes.clone() {
if mailbox_hash == h {
continue;
}
examine_updates(mailbox, &mut conn, &uid_store).await?;
}
conn.send_command(b"IDLE").await?;
let mut blockn = ImapBlockingConnection::from(conn);
let mut watch = std::time::Instant::now();
@ -145,10 +150,7 @@ pub async fn idle(kit: ImapWatchKit) -> Result<()> {
if now.duration_since(watch) >= _5_MINS {
/* Time to poll all inboxes */
let mut conn = timeout(uid_store.timeout, main_conn.lock()).await?;
for (h, mailbox) in mailboxes.clone() {
if mailbox_hash == h {
continue;
}
for (_h, mailbox) in mailboxes.clone() {
examine_updates(mailbox, &mut conn, &uid_store).await?;
}
watch = now;
@ -173,7 +175,7 @@ pub async fn idle(kit: ImapWatchKit) -> Result<()> {
.conn
.read_response(&mut response, RequiredResponses::empty())
.await?;
for l in line.split_rn() {
for l in line.split_rn().chain(response.split_rn()) {
debug!("process_untagged {:?}", &l);
if l.starts_with(b"+ ")
|| l.starts_with(b"* ok")
@ -219,7 +221,6 @@ pub async fn examine_updates(
.examine_mailbox(mailbox_hash, &mut response, true)
.await?
.unwrap();
debug!(&select_response);
{
let mut uidvalidities = uid_store.uidvalidity.lock().unwrap();
@ -244,7 +245,74 @@ pub async fn examine_updates(
uidvalidities.insert(mailbox_hash, select_response.uidvalidity);
}
}
if debug!(select_response.recent > 0) {
if mailbox.is_cold() {
/* Mailbox hasn't been loaded yet */
let has_list_status: bool = conn
.uid_store
.capabilities
.lock()
.unwrap()
.iter()
.any(|cap| cap.eq_ignore_ascii_case(b"LIST-STATUS"));
if has_list_status {
conn.send_command(
format!(
"LIST \"{}\" \"\" RETURN (STATUS (MESSAGES UNSEEN))",
mailbox.imap_path()
)
.as_bytes(),
)
.await?;
conn.read_response(
&mut response,
RequiredResponses::LIST_REQUIRED | RequiredResponses::STATUS,
)
.await?;
debug!(
"list return status out: {}",
String::from_utf8_lossy(&response)
);
for l in response.split_rn() {
if !l.starts_with(b"*") {
continue;
}
if let Ok(status) = protocol_parser::status_response(&l).map(|(_, v)| v) {
if Some(mailbox_hash) == status.mailbox {
if let Some(total) = status.messages {
if let Ok(mut exists_lck) = mailbox.exists.lock() {
exists_lck.clear();
exists_lck.set_not_yet_seen(total);
}
}
if let Some(total) = status.unseen {
if let Ok(mut unseen_lck) = mailbox.unseen.lock() {
unseen_lck.clear();
unseen_lck.set_not_yet_seen(total);
}
}
break;
}
}
}
} else {
conn.send_command(b"SEARCH UNSEEN").await?;
conn.read_response(&mut response, RequiredResponses::SEARCH)
.await?;
let unseen_count = protocol_parser::search_results(&response)?.1.len();
if let Ok(mut exists_lck) = mailbox.exists.lock() {
exists_lck.clear();
exists_lck.set_not_yet_seen(select_response.exists);
}
if let Ok(mut unseen_lck) = mailbox.unseen.lock() {
unseen_lck.clear();
unseen_lck.set_not_yet_seen(unseen_count);
}
}
mailbox.set_warm(true);
return Ok(());
}
if select_response.recent > 0 {
/* UID SEARCH RECENT */
conn.send_command(b"UID SEARCH RECENT").await?;
conn.read_response(&mut response, RequiredResponses::SEARCH)
@ -267,15 +335,17 @@ pub async fn examine_updates(
cmd.push_str(&n.to_string());
}
}
cmd.push_str(" (UID FLAGS RFC822)");
cmd.push_str(
" (UID FLAGS ENVELOPE BODY.PEEK[HEADER.FIELDS (REFERENCES)] BODYSTRUCTURE)",
);
conn.send_command(cmd.as_bytes()).await?;
conn.read_response(&mut response, RequiredResponses::FETCH_REQUIRED)
.await?;
} else if debug!(select_response.exists > mailbox.exists.lock().unwrap().len()) {
} else if select_response.exists > mailbox.exists.lock().unwrap().len() {
conn.send_command(
format!(
"FETCH {}:* (UID FLAGS RFC822)",
mailbox.exists.lock().unwrap().len()
"FETCH {}:* (UID FLAGS ENVELOPE BODY.PEEK[HEADER.FIELDS (REFERENCES)] BODYSTRUCTURE)",
std::cmp::max(mailbox.exists.lock().unwrap().len(), 1)
)
.as_bytes(),
)
@ -285,42 +355,60 @@ pub async fn examine_updates(
} else {
return Ok(());
}
debug!(&response);
debug!(
"fetch response is {} bytes and {} lines",
response.len(),
String::from_utf8_lossy(&response).lines().count()
);
let (_, mut v, _) = protocol_parser::fetch_responses(&response)?;
debug!("responses len is {}", v.len());
for FetchResponse {
ref uid,
ref mut flags,
ref mut body,
ref mut envelope,
ref mut flags,
ref references,
..
} in v.iter_mut()
{
let uid = uid.unwrap();
*envelope = Envelope::from_bytes(body.take().unwrap(), flags.as_ref().map(|&(f, _)| f))
.map(|mut env| {
env.set_hash(generate_envelope_hash(&mailbox.imap_path(), &uid));
if let Some((_, keywords)) = flags.take() {
let mut tag_lck = uid_store.tag_index.write().unwrap();
for f in keywords {
let hash = tag_hash!(f);
if !tag_lck.contains_key(&hash) {
tag_lck.insert(hash, f);
}
env.labels_mut().push(hash);
}
let env = envelope.as_mut().unwrap();
env.set_hash(generate_envelope_hash(&mailbox.imap_path(), &uid));
if let Some(value) = references {
env.set_references(value);
}
let mut tag_lck = uid_store.collection.tag_index.write().unwrap();
if let Some((flags, keywords)) = flags {
env.set_flags(*flags);
if !env.is_seen() {
mailbox.unseen.lock().unwrap().insert_new(env.hash());
}
mailbox.exists.lock().unwrap().insert_new(env.hash());
for f in keywords {
let hash = tag_hash!(f);
if !tag_lck.contains_key(&hash) {
tag_lck.insert(hash, f.to_string());
}
env
})
.map_err(|err| {
debug!("uid {} envelope parse error {}", uid, &err);
err
})
.ok();
env.labels_mut().push(hash);
}
}
}
if uid_store.keep_offline_cache {
cache_handle.insert_envelopes(mailbox_hash, &v)?;
if !cache_handle.mailbox_state(mailbox_hash)?.is_none() {
cache_handle
.insert_envelopes(mailbox_hash, &v)
.chain_err_summary(|| {
format!(
"Could not save envelopes in cache for mailbox {}",
mailbox.imap_path()
)
})?;
}
}
'fetch_responses_c: for FetchResponse { uid, envelope, .. } in v {
for FetchResponse { uid, envelope, .. } in v {
if uid.is_none() || envelope.is_none() {
continue;
}
let uid = uid.unwrap();
if uid_store
.uid_index
@ -328,35 +416,37 @@ pub async fn examine_updates(
.unwrap()
.contains_key(&(mailbox_hash, uid))
{
continue 'fetch_responses_c;
}
if let Some(env) = envelope {
uid_store
.hash_index
.lock()
.unwrap()
.insert(env.hash(), (uid, mailbox_hash));
uid_store
.uid_index
.lock()
.unwrap()
.insert((mailbox_hash, uid), env.hash());
debug!(
"Create event {} {} {}",
env.hash(),
env.subject(),
mailbox.path(),
);
if !env.is_seen() {
mailbox.unseen.lock().unwrap().insert_new(env.hash());
}
mailbox.exists.lock().unwrap().insert_new(env.hash());
conn.add_refresh_event(RefreshEvent {
account_hash: uid_store.account_hash,
mailbox_hash,
kind: Create(Box::new(env)),
});
continue;
}
let env = envelope.unwrap();
debug!(
"Create event {} {} {}",
env.hash(),
env.subject(),
mailbox.path(),
);
uid_store
.msn_index
.lock()
.unwrap()
.entry(mailbox_hash)
.or_default()
.push(uid);
uid_store
.hash_index
.lock()
.unwrap()
.insert(env.hash(), (uid, mailbox_hash));
uid_store
.uid_index
.lock()
.unwrap()
.insert((mailbox_hash, uid), env.hash());
conn.add_refresh_event(RefreshEvent {
account_hash: uid_store.account_hash,
mailbox_hash,
kind: Create(Box::new(env)),
});
}
}
Ok(())

View File

@ -23,16 +23,32 @@ use crate::backends::*;
use crate::conf::AccountSettings;
use crate::email::*;
use crate::error::{MeliError, Result};
use crate::Collection;
use futures::lock::Mutex as FutureMutex;
use isahc::config::RedirectPolicy;
use isahc::prelude::HttpClient;
use isahc::ResponseExt;
use serde_json::Value;
use std::collections::{BTreeMap, HashMap};
use std::collections::{hash_map::DefaultHasher, HashMap, HashSet};
use std::convert::TryFrom;
use std::hash::{Hash, Hasher};
use std::str::FromStr;
use std::sync::{Arc, Mutex, RwLock};
use std::time::Instant;
macro_rules! tag_hash {
($t:ident) => {{
let mut hasher = DefaultHasher::default();
$t.hash(&mut hasher);
hasher.finish()
}};
($t:literal) => {{
let mut hasher = DefaultHasher::default();
$t.hash(&mut hasher);
hasher.finish()
}};
}
#[macro_export]
macro_rules! _impl {
($(#[$outer:meta])*$field:ident : $t:ty) => {
@ -131,20 +147,6 @@ impl JmapServerConf {
}
}
struct IsSubscribedFn(Box<dyn Fn(&str) -> bool + Send + Sync>);
impl std::fmt::Debug for IsSubscribedFn {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "IsSubscribedFn Box")
}
}
impl std::ops::Deref for IsSubscribedFn {
type Target = Box<dyn Fn(&str) -> bool + Send + Sync>;
fn deref(&self) -> &Box<dyn Fn(&str) -> bool + Send + Sync> {
&self.0
}
}
macro_rules! get_conf_val {
($s:ident[$var:literal]) => {
$s.extra.get($var).ok_or_else(|| {
@ -173,24 +175,115 @@ macro_rules! get_conf_val {
};
}
#[derive(Debug, Default)]
#[derive(Debug)]
pub struct Store {
byte_cache: HashMap<EnvelopeHash, EnvelopeCache>,
id_store: HashMap<EnvelopeHash, Id>,
blob_id_store: HashMap<EnvelopeHash, Id>,
pub account_name: Arc<String>,
pub account_hash: AccountHash,
pub account_id: Arc<Mutex<Id<Account>>>,
pub byte_cache: Arc<Mutex<HashMap<EnvelopeHash, EnvelopeCache>>>,
pub id_store: Arc<Mutex<HashMap<EnvelopeHash, Id<EmailObject>>>>,
pub reverse_id_store: Arc<Mutex<HashMap<Id<EmailObject>, EnvelopeHash>>>,
pub blob_id_store: Arc<Mutex<HashMap<EnvelopeHash, Id<BlobObject>>>>,
pub collection: Collection,
pub mailboxes: Arc<RwLock<HashMap<MailboxHash, JmapMailbox>>>,
pub mailboxes_index: Arc<RwLock<HashMap<MailboxHash, HashSet<EnvelopeHash>>>>,
pub mailbox_state: Arc<Mutex<State<MailboxObject>>>,
pub online_status: Arc<FutureMutex<(Instant, Result<()>)>>,
pub is_subscribed: Arc<IsSubscribedFn>,
pub event_consumer: BackendEventConsumer,
}
impl Store {
pub fn add_envelope(&self, obj: EmailObject) -> Envelope {
let mut tag_lck = self.collection.tag_index.write().unwrap();
let tags = obj
.keywords()
.keys()
.map(|tag| {
let tag_hash = {
let mut hasher = DefaultHasher::default();
tag.hash(&mut hasher);
hasher.finish()
};
if !tag_lck.contains_key(&tag_hash) {
tag_lck.insert(tag_hash, tag.to_string());
}
tag_hash
})
.collect::<SmallVec<[u64; 1024]>>();
let id = obj.id.clone();
let mailbox_ids = obj.mailbox_ids.clone();
let blob_id = obj.blob_id.clone();
drop(tag_lck);
let mut ret: Envelope = obj.into();
debug_assert_eq!(tag_hash!("$draft"), 6613915297903591176);
debug_assert_eq!(tag_hash!("$seen"), 1683863812294339685);
debug_assert_eq!(tag_hash!("$flagged"), 2714010747478170100);
debug_assert_eq!(tag_hash!("$answered"), 8940855303929342213);
debug_assert_eq!(tag_hash!("$junk"), 2656839745430720464);
debug_assert_eq!(tag_hash!("$notjunk"), 4091323799684325059);
let mut id_store_lck = self.id_store.lock().unwrap();
let mut reverse_id_store_lck = self.reverse_id_store.lock().unwrap();
let mut blob_id_store_lck = self.blob_id_store.lock().unwrap();
let mailboxes_lck = self.mailboxes.read().unwrap();
let mut mailboxes_index_lck = self.mailboxes_index.write().unwrap();
for (mailbox_id, _) in mailbox_ids {
if let Some((mailbox_hash, _)) = mailboxes_lck.iter().find(|(_, m)| m.id == mailbox_id)
{
mailboxes_index_lck
.entry(*mailbox_hash)
.or_default()
.insert(ret.hash());
}
}
reverse_id_store_lck.insert(id.clone(), ret.hash());
id_store_lck.insert(ret.hash(), id);
blob_id_store_lck.insert(ret.hash(), blob_id);
for t in tags {
match t {
6613915297903591176 => {
ret.set_flags(ret.flags() | Flag::DRAFT);
}
1683863812294339685 => {
ret.set_flags(ret.flags() | Flag::SEEN);
}
2714010747478170100 => {
ret.set_flags(ret.flags() | Flag::FLAGGED);
}
8940855303929342213 => {
ret.set_flags(ret.flags() | Flag::REPLIED);
}
2656839745430720464 | 4091323799684325059 => { /* ignore */ }
_ => ret.labels_mut().push(t),
}
}
ret
}
pub fn remove_envelope(
&self,
obj_id: Id<EmailObject>,
) -> Option<(EnvelopeHash, SmallVec<[MailboxHash; 8]>)> {
let env_hash = self.reverse_id_store.lock().unwrap().remove(&obj_id)?;
self.id_store.lock().unwrap().remove(&env_hash);
self.blob_id_store.lock().unwrap().remove(&env_hash);
self.byte_cache.lock().unwrap().remove(&env_hash);
let mut mailbox_hashes = SmallVec::new();
for (k, set) in self.mailboxes_index.write().unwrap().iter_mut() {
if set.remove(&env_hash) {
mailbox_hashes.push(*k);
}
}
Some((env_hash, mailbox_hashes))
}
}
#[derive(Debug)]
pub struct JmapType {
account_name: String,
account_hash: AccountHash,
online: Arc<FutureMutex<(Instant, Result<()>)>>,
is_subscribed: Arc<IsSubscribedFn>,
server_conf: JmapServerConf,
connection: Arc<FutureMutex<JmapConnection>>,
store: Arc<RwLock<Store>>,
tag_index: Arc<RwLock<BTreeMap<u64, String>>>,
mailboxes: Arc<RwLock<HashMap<MailboxHash, JmapMailbox>>>,
store: Arc<Store>,
}
impl MailBackend for JmapType {
@ -207,7 +300,7 @@ impl MailBackend for JmapType {
}
fn is_online(&self) -> ResultFuture<()> {
let online = self.online.clone();
let online = self.store.online_status.clone();
Ok(Box::pin(async move {
//match timeout(std::time::Duration::from_secs(3), connection.lock()).await {
let online_lck = online.lock().await;
@ -224,9 +317,7 @@ impl MailBackend for JmapType {
&mut self,
mailbox_hash: MailboxHash,
) -> Result<Pin<Box<dyn Stream<Item = Result<Vec<Envelope>>> + Send + 'static>>> {
let mailboxes = self.mailboxes.clone();
let store = self.store.clone();
let tag_index = self.tag_index.clone();
let connection = self.connection.clone();
Ok(Box::pin(async_stream::try_stream! {
let mut conn = connection.lock().await;
@ -234,34 +325,64 @@ impl MailBackend for JmapType {
let res = protocol::fetch(
&conn,
&store,
&tag_index,
&mailboxes,
mailbox_hash,
).await?;
yield res;
}))
}
fn refresh(&mut self, _mailbox_hash: MailboxHash) -> ResultFuture<()> {
Err(MeliError::new("Unimplemented."))
}
fn watch(&self) -> ResultFuture<()> {
Err(MeliError::from("JMAP watch for updates is unimplemented"))
}
fn mailboxes(&self) -> ResultFuture<HashMap<MailboxHash, Mailbox>> {
let mailboxes = self.mailboxes.clone();
fn refresh(&mut self, mailbox_hash: MailboxHash) -> ResultFuture<()> {
let connection = self.connection.clone();
Ok(Box::pin(async move {
let mut conn = connection.lock().await;
conn.connect().await?;
if mailboxes.read().unwrap().is_empty() {
conn.email_changes(mailbox_hash).await?;
Ok(())
}))
}
fn watch(&self) -> ResultFuture<()> {
let connection = self.connection.clone();
let store = self.store.clone();
Ok(Box::pin(async move {
{
let mut conn = connection.lock().await;
conn.connect().await?;
}
loop {
{
let mailbox_hashes = {
store
.mailboxes
.read()
.unwrap()
.keys()
.cloned()
.collect::<SmallVec<[MailboxHash; 16]>>()
};
let conn = connection.lock().await;
for mailbox_hash in mailbox_hashes {
conn.email_changes(mailbox_hash).await?;
}
}
crate::connections::sleep(std::time::Duration::from_secs(60)).await;
}
}))
}
fn mailboxes(&self) -> ResultFuture<HashMap<MailboxHash, Mailbox>> {
let store = self.store.clone();
let connection = self.connection.clone();
Ok(Box::pin(async move {
let mut conn = connection.lock().await;
conn.connect().await?;
if store.mailboxes.read().unwrap().is_empty() {
let new_mailboxes = debug!(protocol::get_mailboxes(&conn).await)?;
*mailboxes.write().unwrap() = new_mailboxes;
*store.mailboxes.write().unwrap() = new_mailboxes;
}
let ret = mailboxes
let ret = store
.mailboxes
.read()
.unwrap()
.iter()
@ -283,15 +404,84 @@ impl MailBackend for JmapType {
fn save(
&self,
_bytes: Vec<u8>,
_mailbox_hash: MailboxHash,
bytes: Vec<u8>,
mailbox_hash: MailboxHash,
_flags: Option<Flag>,
) -> ResultFuture<()> {
Err(MeliError::new("Unimplemented."))
}
let store = self.store.clone();
let connection = self.connection.clone();
Ok(Box::pin(async move {
let mut conn = connection.lock().await;
conn.connect().await?;
/*
* 1. upload binary blob, get blobId
* 2. Email/import
*/
let (api_url, upload_url) = {
let lck = conn.session.lock().unwrap();
(lck.api_url.clone(), lck.upload_url.clone())
};
let mut res = conn
.client
.post_async(
&upload_request_format(upload_url.as_str(), &conn.mail_account_id()),
bytes,
)
.await?;
fn tags(&self) -> Option<Arc<RwLock<BTreeMap<u64, String>>>> {
Some(self.tag_index.clone())
let mailbox_id: Id<MailboxObject> = {
let mailboxes_lck = store.mailboxes.read().unwrap();
if let Some(mailbox) = mailboxes_lck.get(&mailbox_hash) {
mailbox.id.clone()
} else {
return Err(MeliError::new(format!(
"Mailbox with hash {} not found",
mailbox_hash
)));
}
};
let res_text = res.text_async().await?;
let upload_response: UploadResponse = serde_json::from_str(&res_text)?;
let mut req = Request::new(conn.request_no.clone());
let creation_id: Id<EmailObject> = "1".to_string().into();
let mut email_imports = HashMap::default();
let mut mailbox_ids = HashMap::default();
mailbox_ids.insert(mailbox_id, true);
email_imports.insert(
creation_id.clone(),
EmailImport::new()
.blob_id(upload_response.blob_id)
.mailbox_ids(mailbox_ids),
);
let import_call: ImportCall = ImportCall::new()
.account_id(conn.mail_account_id().clone())
.emails(email_imports);
req.add_call(&import_call);
let mut res = conn
.client
.post_async(api_url.as_str(), serde_json::to_string(&req)?)
.await?;
let res_text = res.text_async().await?;
let mut v: MethodResponse = serde_json::from_str(&res_text)?;
let m = ImportResponse::try_from(v.method_responses.remove(0)).or_else(|err| {
let ierr: Result<ImportError> =
serde_json::from_str(&res_text).map_err(|err| err.into());
if let Ok(err) = ierr {
Err(MeliError::new(format!("Could not save message: {:?}", err)))
} else {
Err(err.into())
}
})?;
if let Some(err) = m.not_created.get(&creation_id) {
return Err(MeliError::new(format!("Could not save message: {:?}", err)));
}
Ok(())
}))
}
fn as_any(&self) -> &dyn Any {
@ -302,14 +492,21 @@ impl MailBackend for JmapType {
self
}
fn collection(&self) -> Collection {
self.store.collection.clone()
}
fn search(
&self,
q: crate::search::Query,
mailbox_hash: Option<MailboxHash>,
) -> ResultFuture<SmallVec<[EnvelopeHash; 512]>> {
let store = self.store.clone();
let connection = self.connection.clone();
let filter = if let Some(mailbox_hash) = mailbox_hash {
let mailbox_id = self.mailboxes.read().unwrap()[&mailbox_hash].id.clone();
let mailbox_id = self.store.mailboxes.read().unwrap()[&mailbox_hash]
.id
.clone();
let mut f = Filter::Condition(
EmailFilterCondition::new()
@ -327,7 +524,7 @@ impl MailBackend for JmapType {
conn.connect().await?;
let email_call: EmailQuery = EmailQuery::new(
Query::new()
.account_id(conn.mail_account_id().to_string())
.account_id(conn.mail_account_id().clone())
.filter(Some(filter))
.position(0),
)
@ -335,26 +532,19 @@ impl MailBackend for JmapType {
let mut req = Request::new(conn.request_no.clone());
req.add_call(&email_call);
let api_url = conn.session.lock().unwrap().api_url.clone();
let mut res = conn
.client
.post_async(&conn.session.api_url, serde_json::to_string(&req)?)
.post_async(api_url.as_str(), serde_json::to_string(&req)?)
.await?;
let res_text = res.text_async().await?;
let mut v: MethodResponse = serde_json::from_str(&res_text).unwrap();
*conn.online_status.lock().await = (std::time::Instant::now(), Ok(()));
*store.online_status.lock().await = (std::time::Instant::now(), Ok(()));
let m = QueryResponse::<EmailObject>::try_from(v.method_responses.remove(0))?;
let QueryResponse::<EmailObject> { ids, .. } = m;
let ret = ids
.into_iter()
.map(|id| {
use std::hash::Hasher;
let mut h = std::collections::hash_map::DefaultHasher::new();
h.write(id.as_bytes());
h.finish()
})
.collect();
let ret = ids.into_iter().map(|id| id.into_hash()).collect();
Ok(ret)
}))
}
@ -376,12 +566,92 @@ impl MailBackend for JmapType {
fn copy_messages(
&mut self,
_env_hashes: EnvelopeHashBatch,
_source_mailbox_hash: MailboxHash,
_destination_mailbox_hash: MailboxHash,
_move_: bool,
env_hashes: EnvelopeHashBatch,
source_mailbox_hash: MailboxHash,
destination_mailbox_hash: MailboxHash,
move_: bool,
) -> ResultFuture<()> {
Err(MeliError::new("Unimplemented."))
let store = self.store.clone();
let connection = self.connection.clone();
Ok(Box::pin(async move {
let (source_mailbox_id, destination_mailbox_id) = {
let mailboxes_lck = store.mailboxes.read().unwrap();
if !mailboxes_lck.contains_key(&source_mailbox_hash) {
return Err(MeliError::new(format!(
"Could not find source mailbox with hash {}",
source_mailbox_hash
)));
}
if !mailboxes_lck.contains_key(&destination_mailbox_hash) {
return Err(MeliError::new(format!(
"Could not find destination mailbox with hash {}",
destination_mailbox_hash
)));
}
(
mailboxes_lck[&source_mailbox_hash].id.clone(),
mailboxes_lck[&destination_mailbox_hash].id.clone(),
)
};
let mut update_map: HashMap<Id<EmailObject>, Value> = HashMap::default();
let mut ids: Vec<Id<EmailObject>> = Vec::with_capacity(env_hashes.rest.len() + 1);
let mut id_map: HashMap<Id<EmailObject>, EnvelopeHash> = HashMap::default();
let mut update_keywords: HashMap<String, Value> = HashMap::default();
update_keywords.insert(
format!("mailboxIds/{}", &destination_mailbox_id),
serde_json::json!(true),
);
if move_ {
update_keywords.insert(
format!("mailboxIds/{}", &source_mailbox_id),
serde_json::json!(null),
);
}
{
for env_hash in env_hashes.iter() {
if let Some(id) = store.id_store.lock().unwrap().get(&env_hash) {
ids.push(id.clone());
id_map.insert(id.clone(), env_hash);
update_map.insert(id.clone(), serde_json::json!(update_keywords.clone()));
}
}
}
let conn = connection.lock().await;
let api_url = conn.session.lock().unwrap().api_url.clone();
let email_set_call: EmailSet = EmailSet::new(
Set::<EmailObject>::new()
.account_id(conn.mail_account_id().clone())
.update(Some(update_map)),
);
let mut req = Request::new(conn.request_no.clone());
let _prev_seq = req.add_call(&email_set_call);
let mut res = conn
.client
.post_async(api_url.as_str(), serde_json::to_string(&req)?)
.await?;
let res_text = res.text_async().await?;
let mut v: MethodResponse = serde_json::from_str(&res_text).unwrap();
*store.online_status.lock().await = (std::time::Instant::now(), Ok(()));
let m = SetResponse::<EmailObject>::try_from(v.method_responses.remove(0))?;
if let Some(ids) = m.not_updated {
if !ids.is_empty() {
return Err(MeliError::new(format!(
"Could not update ids: {}",
ids.into_iter()
.map(|err| err.to_string())
.collect::<Vec<String>>()
.join(",")
)));
}
}
Ok(())
}))
}
fn set_flags(
@ -390,16 +660,12 @@ impl MailBackend for JmapType {
mailbox_hash: MailboxHash,
flags: SmallVec<[(std::result::Result<Flag, String>, bool); 8]>,
) -> ResultFuture<()> {
let mailboxes = self.mailboxes.clone();
let store = self.store.clone();
let account_hash = self.account_hash;
let tag_index = self.tag_index.clone();
let connection = self.connection.clone();
Ok(Box::pin(async move {
let mailbox_id = mailboxes.read().unwrap()[&mailbox_hash].id.clone();
let mut update_map: HashMap<String, Value> = HashMap::default();
let mut ids: Vec<Id> = Vec::with_capacity(env_hashes.rest.len() + 1);
let mut id_map: HashMap<Id, EnvelopeHash> = HashMap::default();
let mut update_map: HashMap<Id<EmailObject>, Value> = HashMap::default();
let mut ids: Vec<Id<EmailObject>> = Vec::with_capacity(env_hashes.rest.len() + 1);
let mut id_map: HashMap<Id<EmailObject>, EnvelopeHash> = HashMap::default();
let mut update_keywords: HashMap<String, Value> = HashMap::default();
for (flag, value) in flags.iter() {
match flag {
@ -437,9 +703,8 @@ impl MailBackend for JmapType {
}
}
{
let store_lck = store.read().unwrap();
for hash in env_hashes.iter() {
if let Some(id) = store_lck.id_store.get(&hash) {
if let Some(id) = store.id_store.lock().unwrap().get(&hash) {
ids.push(id.clone());
id_map.insert(id.clone(), hash);
update_map.insert(id.clone(), serde_json::json!(update_keywords.clone()));
@ -450,24 +715,25 @@ impl MailBackend for JmapType {
let email_set_call: EmailSet = EmailSet::new(
Set::<EmailObject>::new()
.account_id(conn.mail_account_id().to_string())
.account_id(conn.mail_account_id().clone())
.update(Some(update_map)),
);
let mut req = Request::new(conn.request_no.clone());
let prev_seq = req.add_call(&email_set_call);
req.add_call(&email_set_call);
let email_call: EmailGet = EmailGet::new(
Get::new()
.ids(Some(JmapArgument::Value(ids)))
.account_id(conn.mail_account_id().to_string())
.account_id(conn.mail_account_id().clone())
.properties(Some(vec!["keywords".to_string()])),
);
req.add_call(&email_call);
let api_url = conn.session.lock().unwrap().api_url.clone();
//debug!(serde_json::to_string(&req)?);
let mut res = conn
.client
.post_async(&conn.session.api_url, serde_json::to_string(&req)?)
.post_async(api_url.as_str(), serde_json::to_string(&req)?)
.await?;
let res_text = res.text_async().await?;
@ -476,7 +742,7 @@ impl MailBackend for JmapType {
*/
//debug!("res_text = {}", &res_text);
let mut v: MethodResponse = serde_json::from_str(&res_text).unwrap();
*conn.online_status.lock().await = (std::time::Instant::now(), Ok(()));
*store.online_status.lock().await = (std::time::Instant::now(), Ok(()));
let m = SetResponse::<EmailObject>::try_from(v.method_responses.remove(0))?;
if let Some(ids) = m.not_updated {
return Err(MeliError::new(
@ -487,24 +753,51 @@ impl MailBackend for JmapType {
));
}
let mut tag_index_lck = tag_index.write().unwrap();
for (flag, value) in flags.iter() {
match flag {
Ok(f) => {}
Err(t) => {
if *value {
tag_index_lck.insert(tag_hash!(t), t.clone());
{
let mut tag_index_lck = store.collection.tag_index.write().unwrap();
for (flag, value) in flags.iter() {
match flag {
Ok(_) => {}
Err(t) => {
if *value {
tag_index_lck.insert(tag_hash!(t), t.clone());
}
}
}
}
drop(tag_index_lck);
}
let e = GetResponse::<EmailObject>::try_from(v.method_responses.pop().unwrap())?;
let GetResponse::<EmailObject> { list, state, .. } = e;
//debug!(&list);
{
let (is_empty, is_equal) = {
let mailboxes_lck = conn.store.mailboxes.read().unwrap();
mailboxes_lck
.get(&mailbox_hash)
.map(|mbox| {
let current_state_lck = mbox.email_state.lock().unwrap();
(
current_state_lck.is_some(),
current_state_lck.as_ref() != Some(&state),
)
})
.unwrap_or((true, true))
};
if is_empty {
let mut mailboxes_lck = conn.store.mailboxes.write().unwrap();
debug!("{:?}: inserting state {}", EmailObject::NAME, &state);
mailboxes_lck.entry(mailbox_hash).and_modify(|mbox| {
*mbox.email_state.lock().unwrap() = Some(state);
});
} else if !is_equal {
conn.email_changes(mailbox_hash).await?;
}
}
debug!(&list);
for envobj in list {
let env_hash = id_map[&envobj.id];
conn.add_refresh_event(RefreshEvent {
account_hash,
account_hash: store.account_hash,
mailbox_hash,
kind: RefreshEventKind::NewFlags(
env_hash,
@ -515,6 +808,14 @@ impl MailBackend for JmapType {
Ok(())
}))
}
fn delete_messages(
&mut self,
_env_hashes: EnvelopeHashBatch,
_mailbox_hash: MailboxHash,
) -> ResultFuture<()> {
Err(MeliError::new("Unimplemented."))
}
}
impl JmapType {
@ -523,34 +824,41 @@ impl JmapType {
is_subscribed: Box<dyn Fn(&str) -> bool + Send + Sync>,
event_consumer: BackendEventConsumer,
) -> Result<Box<dyn MailBackend>> {
let online = Arc::new(FutureMutex::new((
let online_status = Arc::new(FutureMutex::new((
std::time::Instant::now(),
Err(MeliError::new("Account is uninitialised.")),
)));
let server_conf = JmapServerConf::new(s)?;
let account_hash = {
use std::collections::hash_map::DefaultHasher;
use std::hash::Hasher;
let mut hasher = DefaultHasher::new();
hasher.write(s.name.as_bytes());
hasher.finish()
};
let store = Arc::new(Store {
account_name: Arc::new(s.name.clone()),
account_hash,
account_id: Arc::new(Mutex::new(Id::new())),
online_status,
event_consumer,
is_subscribed: Arc::new(IsSubscribedFn(is_subscribed)),
collection: Collection::default(),
byte_cache: Default::default(),
id_store: Default::default(),
reverse_id_store: Default::default(),
blob_id_store: Default::default(),
mailboxes: Default::default(),
mailboxes_index: Default::default(),
mailbox_state: Default::default(),
});
Ok(Box::new(JmapType {
connection: Arc::new(FutureMutex::new(JmapConnection::new(
&server_conf,
account_hash,
event_consumer,
online.clone(),
store.clone(),
)?)),
store: Arc::new(RwLock::new(Store::default())),
tag_index: Arc::new(RwLock::new(Default::default())),
mailboxes: Arc::new(RwLock::new(HashMap::default())),
account_name: s.name.clone(),
account_hash,
online,
is_subscribed: Arc::new(IsSubscribedFn(is_subscribed)),
store,
server_conf,
}))
}

View File

@ -24,26 +24,18 @@ use isahc::config::Configurable;
#[derive(Debug)]
pub struct JmapConnection {
pub session: JmapSession,
pub session: Arc<Mutex<JmapSession>>,
pub request_no: Arc<Mutex<usize>>,
pub client: Arc<HttpClient>,
pub online_status: Arc<FutureMutex<(Instant, Result<()>)>>,
pub server_conf: JmapServerConf,
pub account_id: Arc<Mutex<String>>,
pub account_hash: AccountHash,
pub method_call_states: Arc<Mutex<HashMap<&'static str, String>>>,
pub event_consumer: BackendEventConsumer,
pub store: Arc<Store>,
}
impl JmapConnection {
pub fn new(
server_conf: &JmapServerConf,
account_hash: AccountHash,
event_consumer: BackendEventConsumer,
online_status: Arc<FutureMutex<(Instant, Result<()>)>>,
) -> Result<Self> {
pub fn new(server_conf: &JmapServerConf, store: Arc<Store>) -> Result<Self> {
let client = HttpClient::builder()
.timeout(std::time::Duration::from_secs(10))
.redirect_policy(RedirectPolicy::Limit(10))
.authentication(isahc::auth::Authentication::basic())
.credentials(isahc::auth::Credentials::new(
&server_conf.server_username,
@ -52,20 +44,16 @@ impl JmapConnection {
.build()?;
let server_conf = server_conf.clone();
Ok(JmapConnection {
session: Default::default(),
session: Arc::new(Mutex::new(Default::default())),
request_no: Arc::new(Mutex::new(0)),
client: Arc::new(client),
online_status,
server_conf,
account_id: Arc::new(Mutex::new(String::new())),
account_hash,
event_consumer,
method_call_states: Arc::new(Mutex::new(Default::default())),
store,
})
}
pub async fn connect(&mut self) -> Result<()> {
if self.online_status.lock().await.1.is_ok() {
if self.store.online_status.lock().await.1.is_ok() {
return Ok(());
}
let mut jmap_session_resource_url =
@ -86,7 +74,7 @@ impl JmapConnection {
let session: JmapSession = match serde_json::from_str(&res_text) {
Err(err) => {
let err = MeliError::new(format!("Could not connect to JMAP server endpoint for {}. Is your server hostname setting correct? (i.e. \"jmap.mailserver.org\") (Note: only session resource discovery via /.well-known/jmap is supported. DNS SRV records are not suppported.)\nReply from server: {}", &self.server_conf.server_hostname, &res_text)).set_source(Some(Arc::new(err)));
*self.online_status.lock().await = (Instant::now(), Err(err.clone()));
*self.store.online_status.lock().await = (Instant::now(), Err(err.clone()));
return Err(err);
}
Ok(s) => s,
@ -96,7 +84,7 @@ impl JmapConnection {
.contains_key("urn:ietf:params:jmap:core")
{
let err = MeliError::new(format!("Server {} did not return JMAP Core capability (urn:ietf:params:jmap:core). Returned capabilities were: {}", &self.server_conf.server_hostname, session.capabilities.keys().map(String::as_str).collect::<Vec<&str>>().join(", ")));
*self.online_status.lock().await = (Instant::now(), Err(err.clone()));
*self.store.online_status.lock().await = (Instant::now(), Err(err.clone()));
return Err(err);
}
if !session
@ -104,20 +92,251 @@ impl JmapConnection {
.contains_key("urn:ietf:params:jmap:mail")
{
let err = MeliError::new(format!("Server {} does not support JMAP Mail capability (urn:ietf:params:jmap:mail). Returned capabilities were: {}", &self.server_conf.server_hostname, session.capabilities.keys().map(String::as_str).collect::<Vec<&str>>().join(", ")));
*self.online_status.lock().await = (Instant::now(), Err(err.clone()));
*self.store.online_status.lock().await = (Instant::now(), Err(err.clone()));
return Err(err);
}
*self.online_status.lock().await = (Instant::now(), Ok(()));
self.session = session;
*self.store.online_status.lock().await = (Instant::now(), Ok(()));
*self.session.lock().unwrap() = session;
Ok(())
}
pub fn mail_account_id(&self) -> &Id {
&self.session.primary_accounts["urn:ietf:params:jmap:mail"]
pub fn mail_account_id(&self) -> Id<Account> {
self.session.lock().unwrap().primary_accounts["urn:ietf:params:jmap:mail"].clone()
}
pub fn add_refresh_event(&self, event: RefreshEvent) {
(self.event_consumer)(self.account_hash, BackendEvent::Refresh(event));
(self.store.event_consumer)(self.store.account_hash, BackendEvent::Refresh(event));
}
pub async fn email_changes(&self, mailbox_hash: MailboxHash) -> Result<()> {
let mut current_state: State<EmailObject> = if let Some(s) = self
.store
.mailboxes
.read()
.unwrap()
.get(&mailbox_hash)
.and_then(|mbox| mbox.email_state.lock().unwrap().clone())
{
s
} else {
return Ok(());
};
loop {
let email_changes_call: EmailChanges = EmailChanges::new(
Changes::<EmailObject>::new()
.account_id(self.mail_account_id().clone())
.since_state(current_state.clone()),
);
let mut req = Request::new(self.request_no.clone());
let prev_seq = req.add_call(&email_changes_call);
let email_get_call: EmailGet = EmailGet::new(
Get::new()
.ids(Some(JmapArgument::reference(
prev_seq,
ResultField::<EmailChanges, EmailObject>::new("created"),
)))
.account_id(self.mail_account_id().clone()),
);
req.add_call(&email_get_call);
if let Some(mailbox) = self.store.mailboxes.read().unwrap().get(&mailbox_hash) {
if let Some(email_query_state) = mailbox.email_query_state.lock().unwrap().clone() {
let email_query_changes_call = EmailQueryChanges::new(
QueryChanges::new(self.mail_account_id().clone(), email_query_state)
.filter(Some(Filter::Condition(
EmailFilterCondition::new()
.in_mailbox(Some(mailbox.id.clone()))
.into(),
))),
);
let seq_no = req.add_call(&email_query_changes_call);
let email_get_call: EmailGet = EmailGet::new(
Get::new()
.ids(Some(JmapArgument::reference(
seq_no,
ResultField::<EmailQueryChanges, EmailObject>::new("removed"),
)))
.account_id(self.mail_account_id().clone())
.properties(Some(vec![
"keywords".to_string(),
"mailboxIds".to_string(),
])),
);
req.add_call(&email_get_call);
} else {
return Ok(());
}
} else {
return Ok(());
}
let api_url = self.session.lock().unwrap().api_url.clone();
let mut res = self
.client
.post_async(api_url.as_str(), serde_json::to_string(&req)?)
.await?;
let res_text = res.text_async().await?;
debug!(&res_text);
let mut v: MethodResponse = serde_json::from_str(&res_text).unwrap();
let changes_response =
ChangesResponse::<EmailObject>::try_from(v.method_responses.remove(0))?;
if changes_response.new_state == current_state {
return Ok(());
}
let get_response = GetResponse::<EmailObject>::try_from(v.method_responses.remove(0))?;
{
/* process get response */
let GetResponse::<EmailObject> { list, .. } = get_response;
let mut mailbox_hashes: Vec<SmallVec<[MailboxHash; 8]>> =
Vec::with_capacity(list.len());
for envobj in &list {
let v = self
.store
.mailboxes
.read()
.unwrap()
.iter()
.filter(|(_, m)| envobj.mailbox_ids.contains_key(&m.id))
.map(|(k, _)| *k)
.collect::<SmallVec<[MailboxHash; 8]>>();
mailbox_hashes.push(v);
}
for (env, mailbox_hashes) in list
.into_iter()
.map(|obj| self.store.add_envelope(obj))
.zip(mailbox_hashes)
{
for mailbox_hash in mailbox_hashes.iter().skip(1).cloned() {
let mut mailboxes_lck = self.store.mailboxes.write().unwrap();
mailboxes_lck.entry(mailbox_hash).and_modify(|mbox| {
if !env.is_seen() {
mbox.unread_emails.lock().unwrap().insert_new(env.hash());
}
mbox.total_emails.lock().unwrap().insert_new(env.hash());
});
self.add_refresh_event(RefreshEvent {
account_hash: self.store.account_hash,
mailbox_hash,
kind: RefreshEventKind::Create(Box::new(env.clone())),
});
}
if let Some(mailbox_hash) = mailbox_hashes.first().cloned() {
let mut mailboxes_lck = self.store.mailboxes.write().unwrap();
mailboxes_lck.entry(mailbox_hash).and_modify(|mbox| {
if !env.is_seen() {
mbox.unread_emails.lock().unwrap().insert_new(env.hash());
}
mbox.total_emails.lock().unwrap().insert_new(env.hash());
});
self.add_refresh_event(RefreshEvent {
account_hash: self.store.account_hash,
mailbox_hash,
kind: RefreshEventKind::Create(Box::new(env)),
});
}
}
}
let reverse_id_store_lck = self.store.reverse_id_store.lock().unwrap();
let response = v.method_responses.remove(0);
match EmailQueryChangesResponse::try_from(response) {
Ok(EmailQueryChangesResponse {
collapse_threads: _,
query_changes_response:
QueryChangesResponse {
account_id: _,
old_query_state,
new_query_state,
total: _,
removed,
added,
},
}) if old_query_state != new_query_state => {
self.store
.mailboxes
.write()
.unwrap()
.entry(mailbox_hash)
.and_modify(|mbox| {
*mbox.email_query_state.lock().unwrap() = Some(new_query_state);
});
/* If the "filter" or "sort" includes a mutable property, the server
MUST include all Foos in the current results for which this
property may have changed. The position of these may have moved
in the results, so they must be reinserted by the client to ensure
its query cache is correct. */
for email_obj_id in removed
.into_iter()
.filter(|id| !added.iter().any(|item| item.id == *id))
{
if let Some(env_hash) = reverse_id_store_lck.get(&email_obj_id) {
let mut mailboxes_lck = self.store.mailboxes.write().unwrap();
mailboxes_lck.entry(mailbox_hash).and_modify(|mbox| {
mbox.unread_emails.lock().unwrap().remove(*env_hash);
mbox.total_emails.lock().unwrap().insert_new(*env_hash);
});
self.add_refresh_event(RefreshEvent {
account_hash: self.store.account_hash,
mailbox_hash,
kind: RefreshEventKind::Remove(*env_hash),
});
}
}
for AddedItem {
id: _email_obj_id,
index: _,
} in added
{
// FIXME
}
}
Ok(_) => {}
Err(err) => {
debug!(mailbox_hash);
debug!(err);
}
}
let GetResponse::<EmailObject> { list, .. } =
GetResponse::<EmailObject>::try_from(v.method_responses.remove(0))?;
let mut mailboxes_lck = self.store.mailboxes.write().unwrap();
for envobj in list {
if let Some(env_hash) = reverse_id_store_lck.get(&envobj.id) {
let new_flags =
protocol::keywords_to_flags(envobj.keywords().keys().cloned().collect());
mailboxes_lck.entry(mailbox_hash).and_modify(|mbox| {
if new_flags.0.contains(Flag::SEEN) {
mbox.unread_emails.lock().unwrap().remove(*env_hash);
} else {
mbox.unread_emails.lock().unwrap().insert_new(*env_hash);
}
});
self.add_refresh_event(RefreshEvent {
account_hash: self.store.account_hash,
mailbox_hash,
kind: RefreshEventKind::NewFlags(*env_hash, new_flags),
});
}
}
drop(mailboxes_lck);
if changes_response.has_more_changes {
current_state = changes_response.new_state;
} else {
self.store
.mailboxes
.write()
.unwrap()
.entry(mailbox_hash)
.and_modify(|mbox| {
*mbox.email_state.lock().unwrap() = Some(changes_response.new_state);
});
break;
}
}
Ok(())
}
}

View File

@ -20,7 +20,7 @@
*/
use super::*;
use crate::backends::{MailboxPermissions, SpecialUsageMailbox};
use crate::backends::{LazyCountSet, MailboxPermissions, SpecialUsageMailbox};
use std::sync::{Arc, Mutex, RwLock};
#[derive(Debug, Clone)]
@ -28,18 +28,21 @@ pub struct JmapMailbox {
pub name: String,
pub path: String,
pub hash: MailboxHash,
pub v: Vec<MailboxHash>,
pub id: String,
pub children: Vec<MailboxHash>,
pub id: Id<MailboxObject>,
pub is_subscribed: bool,
pub my_rights: JmapRights,
pub parent_id: Option<String>,
pub parent_id: Option<Id<MailboxObject>>,
pub parent_hash: Option<MailboxHash>,
pub role: Option<String>,
pub sort_order: u64,
pub total_emails: Arc<Mutex<u64>>,
pub total_emails: Arc<Mutex<LazyCountSet>>,
pub total_threads: u64,
pub unread_emails: Arc<Mutex<u64>>,
pub unread_emails: Arc<Mutex<LazyCountSet>>,
pub unread_threads: u64,
pub usage: Arc<RwLock<SpecialUsageMailbox>>,
pub email_state: Arc<Mutex<Option<State<EmailObject>>>>,
pub email_query_state: Arc<Mutex<Option<String>>>,
}
impl BackendMailbox for JmapMailbox {
@ -62,11 +65,11 @@ impl BackendMailbox for JmapMailbox {
}
fn children(&self) -> &[MailboxHash] {
&self.v
&self.children
}
fn parent(&self) -> Option<MailboxHash> {
None
self.parent_hash
}
fn permissions(&self) -> MailboxPermissions {
@ -108,8 +111,8 @@ impl BackendMailbox for JmapMailbox {
fn count(&self) -> Result<(usize, usize)> {
Ok((
*self.unread_emails.lock()? as usize,
*self.total_emails.lock()? as usize,
self.unread_emails.lock()?.len(),
self.total_emails.lock()?.len(),
))
}
}

View File

@ -24,11 +24,30 @@ use crate::backends::jmap::rfc8620::bool_false;
use crate::email::address::{Address, MailboxAddress};
use core::marker::PhantomData;
use serde::de::{Deserialize, Deserializer};
use serde_json::value::RawValue;
use serde_json::Value;
use std::collections::hash_map::DefaultHasher;
use std::collections::HashMap;
use std::hash::Hasher;
mod import;
pub use import::*;
#[derive(Debug)]
pub struct ThreadObject;
impl Object for ThreadObject {
const NAME: &'static str = "Thread";
}
impl Id<EmailObject> {
pub fn into_hash(&self) -> EnvelopeHash {
let mut h = DefaultHasher::new();
h.write(self.inner.as_bytes());
h.finish()
}
}
// 4.1.1.
// Metadata
// These properties represent metadata about the message in the mail
@ -130,58 +149,58 @@ use std::hash::Hasher;
#[serde(rename_all = "camelCase")]
pub struct EmailObject {
#[serde(default)]
pub id: Id,
pub id: Id<EmailObject>,
#[serde(default)]
pub blob_id: String,
pub blob_id: Id<BlobObject>,
#[serde(default)]
mailbox_ids: HashMap<Id, bool>,
pub mailbox_ids: HashMap<Id<MailboxObject>, bool>,
#[serde(default)]
size: u64,
pub size: u64,
#[serde(default)]
received_at: String,
pub received_at: String,
#[serde(default)]
message_id: Vec<String>,
pub message_id: Vec<String>,
#[serde(default)]
to: SmallVec<[EmailAddress; 1]>,
pub to: Option<SmallVec<[EmailAddress; 1]>>,
#[serde(default)]
bcc: Option<Vec<EmailAddress>>,
pub bcc: Option<Vec<EmailAddress>>,
#[serde(default)]
reply_to: Option<Vec<EmailAddress>>,
pub reply_to: Option<Vec<EmailAddress>>,
#[serde(default)]
cc: Option<SmallVec<[EmailAddress; 1]>>,
pub cc: Option<SmallVec<[EmailAddress; 1]>>,
#[serde(default)]
sender: Option<Vec<EmailAddress>>,
pub sender: Option<Vec<EmailAddress>>,
#[serde(default)]
from: SmallVec<[EmailAddress; 1]>,
pub from: Option<SmallVec<[EmailAddress; 1]>>,
#[serde(default)]
in_reply_to: Option<Vec<String>>,
pub in_reply_to: Option<Vec<String>>,
#[serde(default)]
references: Option<Vec<String>>,
pub references: Option<Vec<String>>,
#[serde(default)]
keywords: HashMap<String, bool>,
pub keywords: HashMap<String, bool>,
#[serde(default)]
attached_emails: Option<Id>,
pub attached_emails: Option<Id<BlobObject>>,
#[serde(default)]
attachments: Vec<Value>,
pub attachments: Vec<Value>,
#[serde(default)]
has_attachment: bool,
pub has_attachment: bool,
#[serde(default)]
#[serde(deserialize_with = "deserialize_header")]
headers: HashMap<String, String>,
pub headers: HashMap<String, String>,
#[serde(default)]
html_body: Vec<HtmlBody>,
pub html_body: Vec<HtmlBody>,
#[serde(default)]
preview: Option<String>,
pub preview: Option<String>,
#[serde(default)]
sent_at: Option<String>,
pub sent_at: Option<String>,
#[serde(default)]
subject: Option<String>,
pub subject: Option<String>,
#[serde(default)]
text_body: Vec<TextBody>,
pub text_body: Vec<TextBody>,
#[serde(default)]
thread_id: Id,
pub thread_id: Id<ThreadObject>,
#[serde(flatten)]
extra: HashMap<String, Value>,
pub extra: HashMap<String, Value>,
}
impl EmailObject {
@ -190,9 +209,9 @@ impl EmailObject {
#[derive(Deserialize, Serialize, Debug, Default)]
#[serde(rename_all = "camelCase")]
struct Header {
name: String,
value: String,
pub struct Header {
pub name: String,
pub value: String,
}
fn deserialize_header<'de, D>(
@ -207,9 +226,9 @@ where
#[derive(Deserialize, Serialize, Debug, Default)]
#[serde(rename_all = "camelCase")]
struct EmailAddress {
email: String,
name: Option<String>,
pub struct EmailAddress {
pub email: String,
pub name: Option<String>,
}
impl Into<crate::email::Address> for EmailAddress {
@ -247,15 +266,11 @@ impl std::convert::From<EmailObject> for crate::Envelope {
}
if let Some(ref in_reply_to) = t.in_reply_to {
env.set_in_reply_to(in_reply_to[0].as_bytes());
env.push_references(env.in_reply_to().unwrap().clone());
if let Some(in_reply_to) = env.in_reply_to().cloned() {
env.push_references(in_reply_to);
}
}
if let Some(v) = t.headers.get("References") {
let parse_result = crate::email::parser::address::msg_id_list(v.as_bytes());
if let Ok((_, v)) = parse_result {
for v in v {
env.push_references(v);
}
}
env.set_references(v.as_bytes());
}
if let Some(v) = t.headers.get("Date") {
@ -263,24 +278,30 @@ impl std::convert::From<EmailObject> for crate::Envelope {
if let Ok(d) = crate::email::parser::dates::rfc5322_date(v.as_bytes()) {
env.set_datetime(d);
}
} else if let Ok(d) = crate::email::parser::dates::rfc5322_date(t.received_at.as_bytes()) {
env.set_datetime(d);
}
env.set_has_attachments(t.has_attachment);
if let Some(ref mut subject) = t.subject {
env.set_subject(std::mem::replace(subject, String::new()).into_bytes());
}
env.set_from(
std::mem::replace(&mut t.from, SmallVec::new())
.into_iter()
.map(|addr| addr.into())
.collect::<SmallVec<[crate::email::Address; 1]>>(),
);
env.set_to(
std::mem::replace(&mut t.to, SmallVec::new())
.into_iter()
.map(|addr| addr.into())
.collect::<SmallVec<[crate::email::Address; 1]>>(),
);
if let Some(ref mut from) = t.from {
env.set_from(
std::mem::replace(from, SmallVec::new())
.into_iter()
.map(|addr| addr.into())
.collect::<SmallVec<[crate::email::Address; 1]>>(),
);
}
if let Some(ref mut to) = t.to {
env.set_to(
std::mem::replace(to, SmallVec::new())
.into_iter()
.map(|addr| addr.into())
.collect::<SmallVec<[crate::email::Address; 1]>>(),
);
}
if let Some(ref mut cc) = t.cc {
env.set_cc(
@ -300,99 +321,75 @@ impl std::convert::From<EmailObject> for crate::Envelope {
);
}
if env.references.is_some() {
if let Some(pos) = env
.references
.as_ref()
.map(|r| &r.refs)
.unwrap()
.iter()
.position(|r| r == env.message_id())
{
if let Some(ref r) = env.references {
if let Some(pos) = r.refs.iter().position(|r| r == env.message_id()) {
env.references.as_mut().unwrap().refs.remove(pos);
}
}
let mut h = DefaultHasher::new();
h.write(t.id.as_bytes());
env.set_hash(h.finish());
env.set_hash(t.id.into_hash());
env
}
}
#[derive(Deserialize, Serialize, Debug)]
#[serde(rename_all = "camelCase")]
struct HtmlBody {
blob_id: Id,
pub struct HtmlBody {
pub blob_id: Id<BlobObject>,
#[serde(default)]
charset: String,
pub charset: String,
#[serde(default)]
cid: Option<String>,
pub cid: Option<String>,
#[serde(default)]
disposition: Option<String>,
pub disposition: Option<String>,
#[serde(default)]
headers: Value,
pub headers: Value,
#[serde(default)]
language: Option<Vec<String>>,
pub language: Option<Vec<String>>,
#[serde(default)]
location: Option<String>,
pub location: Option<String>,
#[serde(default)]
name: Option<String>,
pub name: Option<String>,
#[serde(default)]
part_id: Option<String>,
size: u64,
pub part_id: Option<String>,
pub size: u64,
#[serde(alias = "type")]
content_type: String,
pub content_type: String,
#[serde(default)]
sub_parts: Vec<Value>,
pub sub_parts: Vec<Value>,
}
#[derive(Deserialize, Serialize, Debug)]
#[serde(rename_all = "camelCase")]
struct TextBody {
blob_id: Id,
pub struct TextBody {
pub blob_id: Id<BlobObject>,
#[serde(default)]
charset: String,
pub charset: String,
#[serde(default)]
cid: Option<String>,
pub cid: Option<String>,
#[serde(default)]
disposition: Option<String>,
pub disposition: Option<String>,
#[serde(default)]
headers: Value,
pub headers: Value,
#[serde(default)]
language: Option<Vec<String>>,
pub language: Option<Vec<String>>,
#[serde(default)]
location: Option<String>,
pub location: Option<String>,
#[serde(default)]
name: Option<String>,
pub name: Option<String>,
#[serde(default)]
part_id: Option<String>,
size: u64,
pub part_id: Option<String>,
pub size: u64,
#[serde(alias = "type")]
content_type: String,
pub content_type: String,
#[serde(default)]
sub_parts: Vec<Value>,
pub sub_parts: Vec<Value>,
}
impl Object for EmailObject {
const NAME: &'static str = "Email";
}
#[derive(Deserialize, Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct EmailQueryResponse {
pub account_id: Id,
pub can_calculate_changes: bool,
pub collapse_threads: bool,
// FIXME
pub filter: String,
pub ids: Vec<Id>,
pub position: u64,
pub query_state: String,
pub sort: Option<String>,
pub total: usize,
}
#[derive(Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct EmailQuery {
@ -469,9 +466,9 @@ impl EmailGet {
#[serde(rename_all = "camelCase")]
pub struct EmailFilterCondition {
#[serde(skip_serializing_if = "Option::is_none")]
pub in_mailbox: Option<Id>,
pub in_mailbox: Option<Id<MailboxObject>>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub in_mailbox_other_than: Vec<Id>,
pub in_mailbox_other_than: Vec<Id<MailboxObject>>,
#[serde(skip_serializing_if = "String::is_empty")]
pub before: UtcDate,
#[serde(skip_serializing_if = "String::is_empty")]
@ -517,8 +514,8 @@ impl EmailFilterCondition {
Self::default()
}
_impl!(in_mailbox: Option<Id>);
_impl!(in_mailbox_other_than: Vec<Id>);
_impl!(in_mailbox: Option<Id<MailboxObject>>);
_impl!(in_mailbox_other_than: Vec<Id<MailboxObject>>);
_impl!(before: UtcDate);
_impl!(after: UtcDate);
_impl!(min_size: Option<u64>);
@ -582,6 +579,7 @@ impl From<crate::search::Query> for Filter<EmailFilterCondition, EmailObject> {
fn from(val: crate::search::Query) -> Self {
let mut ret = Filter::Condition(EmailFilterCondition::new().into());
fn rec(q: &crate::search::Query, f: &mut Filter<EmailFilterCondition, EmailObject>) {
use crate::datetime::{timestamp_to_string, RFC3339_FMT};
use crate::search::Query::*;
match q {
Subject(t) => {
@ -605,23 +603,48 @@ impl From<crate::search::Query> for Filter<EmailFilterCondition, EmailObject> {
Body(t) => {
*f = Filter::Condition(EmailFilterCondition::new().body(t.clone()).into());
}
Before(_) => {
//TODO, convert UNIX timestamp into UtcDate
Before(t) => {
*f = Filter::Condition(
EmailFilterCondition::new()
.before(timestamp_to_string(*t, Some(RFC3339_FMT), true))
.into(),
);
}
After(_) => {
//TODO
After(t) => {
*f = Filter::Condition(
EmailFilterCondition::new()
.after(timestamp_to_string(*t, Some(RFC3339_FMT), true))
.into(),
);
}
Between(_, _) => {
//TODO
Between(a, b) => {
*f = Filter::Condition(
EmailFilterCondition::new()
.after(timestamp_to_string(*a, Some(RFC3339_FMT), true))
.into(),
);
*f &= Filter::Condition(
EmailFilterCondition::new()
.before(timestamp_to_string(*b, Some(RFC3339_FMT), true))
.into(),
);
}
On(_) => {
//TODO
On(t) => {
rec(&Between(*t, *t), f);
}
InReplyTo(_) => {
//TODO, look inside Headers
InReplyTo(ref s) => {
*f = Filter::Condition(
EmailFilterCondition::new()
.header(vec!["In-Reply-To".to_string().into(), s.to_string().into()])
.into(),
);
}
References(_) => {
//TODO
References(ref s) => {
*f = Filter::Condition(
EmailFilterCondition::new()
.header(vec!["References".to_string().into(), s.to_string().into()])
.into(),
);
}
AllAddresses(_) => {
//TODO
@ -728,7 +751,7 @@ fn test_jmap_query() {
let mut r = Filter::Condition(
EmailFilterCondition::new()
.in_mailbox(Some(mailbox_id))
.in_mailbox(Some(mailbox_id.into()))
.into(),
);
r &= f;
@ -737,7 +760,7 @@ fn test_jmap_query() {
let email_call: EmailQuery = EmailQuery::new(
Query::new()
.account_id("account_id".to_string())
.account_id("account_id".to_string().into())
.filter(Some(filter))
.position(0),
)
@ -770,3 +793,57 @@ impl EmailSet {
EmailSet { set_call }
}
}
#[derive(Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct EmailChanges {
#[serde(flatten)]
pub changes_call: Changes<EmailObject>,
}
impl Method<EmailObject> for EmailChanges {
const NAME: &'static str = "Email/changes";
}
impl EmailChanges {
pub fn new(changes_call: Changes<EmailObject>) -> Self {
EmailChanges { changes_call }
}
}
#[derive(Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct EmailQueryChanges {
#[serde(flatten)]
pub query_changes_call: QueryChanges<Filter<EmailFilterCondition, EmailObject>, EmailObject>,
}
impl Method<EmailObject> for EmailQueryChanges {
const NAME: &'static str = "Email/queryChanges";
}
impl EmailQueryChanges {
pub fn new(
query_changes_call: QueryChanges<Filter<EmailFilterCondition, EmailObject>, EmailObject>,
) -> Self {
EmailQueryChanges { query_changes_call }
}
}
#[derive(Deserialize, Serialize, Debug)]
pub struct EmailQueryChangesResponse {
///o The "collapseThreads" argument that was used with "Email/query".
#[serde(default = "bool_false")]
pub collapse_threads: bool,
#[serde(flatten)]
pub query_changes_response: QueryChangesResponse<EmailObject>,
}
impl std::convert::TryFrom<&RawValue> for EmailQueryChangesResponse {
type Error = crate::error::MeliError;
fn try_from(t: &RawValue) -> Result<EmailQueryChangesResponse> {
let res: (String, EmailQueryChangesResponse, String) = serde_json::from_str(t.get())?;
assert_eq!(&res.0, "Email/queryChanges");
Ok(res.1)
}
}

View File

@ -0,0 +1,200 @@
/*
* meli -
*
* Copyright Manos Pitsidianakis
*
* This file is part of meli.
*
* meli is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* meli is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
use super::*;
use serde_json::value::RawValue;
/// #`import`
///
/// Objects of type `Foo` are imported via a call to `Foo/import`.
///
/// It takes the following arguments:
///
/// - `account_id`: "Id"
///
/// The id of the account to use.
///
#[derive(Deserialize, Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct ImportCall {
///accountId: "Id"
///The id of the account to use.
pub account_id: Id<Account>,
///ifInState: "String|null"
///This is a state string as returned by the "Email/get" method. If
///supplied, the string must match the current state of the account
///referenced by the accountId; otherwise, the method will be aborted
///and a "stateMismatch" error returned. If null, any changes will
///be applied to the current state.
#[serde(skip_serializing_if = "Option::is_none")]
pub if_in_state: Option<State<EmailObject>>,
///o emails: "Id[EmailImport]"
///A map of creation id (client specified) to EmailImport objects.
pub emails: HashMap<Id<EmailObject>, EmailImport>,
}
#[derive(Deserialize, Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct EmailImport {
///o blobId: "Id"
///The id of the blob containing the raw message [RFC5322].
pub blob_id: Id<BlobObject>,
///o mailboxIds: "Id[Boolean]"
///The ids of the Mailboxes to assign this Email to. At least one
///Mailbox MUST be given.
pub mailbox_ids: HashMap<Id<MailboxObject>, bool>,
///o keywords: "String[Boolean]" (default: {})
///The keywords to apply to the Email.
pub keywords: HashMap<String, bool>,
///o receivedAt: "UTCDate" (default: time of most recent Received
///header, or time of import on server if none)
///The "receivedAt" date to set on the Email.
pub received_at: Option<String>,
}
impl ImportCall {
pub fn new() -> Self {
Self {
account_id: Id::new(),
if_in_state: None,
emails: HashMap::default(),
}
}
_impl!(
/// - accountId: "Id"
///
/// The id of the account to use.
///
account_id: Id<Account>
);
_impl!(if_in_state: Option<State<EmailObject>>);
_impl!(emails: HashMap<Id<EmailObject>, EmailImport>);
}
impl Method<EmailObject> for ImportCall {
const NAME: &'static str = "Email/import";
}
impl EmailImport {
pub fn new() -> Self {
Self {
blob_id: Id::new(),
mailbox_ids: HashMap::default(),
keywords: HashMap::default(),
received_at: None,
}
}
_impl!(blob_id: Id<BlobObject>);
_impl!(mailbox_ids: HashMap<Id<MailboxObject>, bool>);
_impl!(keywords: HashMap<String, bool>);
_impl!(received_at: Option<String>);
}
#[derive(Deserialize, Serialize, Debug)]
#[serde(rename_all = "camelCase")]
#[serde(tag = "type")]
pub enum ImportError {
///The server MAY forbid two Email objects with the same exact content
/// [RFC5322], or even just with the same Message-ID [RFC5322], to
/// coexist within an account. In this case, it MUST reject attempts to
/// import an Email considered to be a duplicate with an "alreadyExists"
/// SetError.
AlreadyExists {
description: Option<String>,
/// An "existingId" property of type "Id" MUST be included on
///the SetError object with the id of the existing Email. If duplicates
///are allowed, the newly created Email object MUST have a separate id
///and independent mutable properties to the existing object.
existing_id: Id<EmailObject>,
},
///If the "blobId", "mailboxIds", or "keywords" properties are invalid
///(e.g., missing, wrong type, id not found), the server MUST reject the
///import with an "invalidProperties" SetError.
InvalidProperties {
description: Option<String>,
properties: Vec<String>,
},
///If the Email cannot be imported because it would take the account
///over quota, the import should be rejected with an "overQuota"
///SetError.
OverQuota { description: Option<String> },
///If the blob referenced is not a valid message [RFC5322], the server
///MAY modify the message to fix errors (such as removing NUL octets or
///fixing invalid headers). If it does this, the "blobId" on the
///response MUST represent the new representation and therefore be
///different to the "blobId" on the EmailImport object. Alternatively,
///the server MAY reject the import with an "invalidEmail" SetError.
InvalidEmail { description: Option<String> },
///An "ifInState" argument was supplied, and it does not match the current state.
StateMismatch,
}
#[derive(Deserialize, Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct ImportResponse {
///o accountId: "Id"
///The id of the account used for this call.
pub account_id: Id<Account>,
///o oldState: "String|null"
///The state string that would have been returned by "Email/get" on
///this account before making the requested changes, or null if the
///server doesn't know what the previous state string was.
pub old_state: Option<State<EmailObject>>,
///o newState: "String"
///The state string that will now be returned by "Email/get" on this
///account.
pub new_state: Option<State<EmailObject>>,
///o created: "Id[Email]|null"
///A map of the creation id to an object containing the "id",
///"blobId", "threadId", and "size" properties for each successfully
///imported Email, or null if none.
pub created: HashMap<Id<EmailObject>, ImportEmailResult>,
///o notCreated: "Id[SetError]|null"
///A map of the creation id to a SetError object for each Email that
///failed to be created, or null if all successful. The possible
///errors are defined above.
pub not_created: HashMap<Id<EmailObject>, ImportError>,
}
impl std::convert::TryFrom<&RawValue> for ImportResponse {
type Error = crate::error::MeliError;
fn try_from(t: &RawValue) -> Result<ImportResponse> {
let res: (String, ImportResponse, String) = serde_json::from_str(t.get())?;
assert_eq!(&res.0, &ImportCall::NAME);
Ok(res.1)
}
}
#[derive(Deserialize, Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct ImportEmailResult {
pub id: Id<EmailObject>,
pub blob_id: Id<BlobObject>,
pub thread_id: Id<ThreadObject>,
pub size: usize,
}

View File

@ -21,14 +21,22 @@
use super::*;
impl Id<MailboxObject> {
pub fn into_hash(&self) -> MailboxHash {
let mut h = DefaultHasher::new();
h.write(self.inner.as_bytes());
h.finish()
}
}
#[derive(Deserialize, Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct MailboxObject {
pub id: String,
pub id: Id<MailboxObject>,
pub is_subscribed: bool,
pub my_rights: JmapRights,
pub name: String,
pub parent_id: Option<String>,
pub parent_id: Option<Id<MailboxObject>>,
pub role: Option<String>,
pub sort_order: u64,
pub total_emails: u64,

View File

@ -20,21 +20,21 @@
*/
use super::*;
use std::sync::{Arc, RwLock};
use std::sync::Arc;
/// `BackendOp` implementor for Imap
#[derive(Debug, Clone)]
pub struct JmapOp {
hash: EnvelopeHash,
connection: Arc<FutureMutex<JmapConnection>>,
store: Arc<RwLock<Store>>,
store: Arc<Store>,
}
impl JmapOp {
pub fn new(
hash: EnvelopeHash,
connection: Arc<FutureMutex<JmapConnection>>,
store: Arc<RwLock<Store>>,
store: Arc<Store>,
) -> Self {
JmapOp {
hash,
@ -47,11 +47,9 @@ impl JmapOp {
impl BackendOp for JmapOp {
fn as_bytes(&mut self) -> ResultFuture<Vec<u8>> {
{
let store_lck = self.store.read().unwrap();
if store_lck.byte_cache.contains_key(&self.hash)
&& store_lck.byte_cache[&self.hash].bytes.is_some()
{
let ret = store_lck.byte_cache[&self.hash].bytes.clone().unwrap();
let byte_lck = self.store.byte_cache.lock().unwrap();
if byte_lck.contains_key(&self.hash) && byte_lck[&self.hash].bytes.is_some() {
let ret = byte_lck[&self.hash].bytes.clone().unwrap();
return Ok(Box::pin(async move { Ok(ret.into_bytes()) }));
}
}
@ -59,14 +57,15 @@ impl BackendOp for JmapOp {
let hash = self.hash;
let connection = self.connection.clone();
Ok(Box::pin(async move {
let blob_id = store.read().unwrap().blob_id_store[&hash].clone();
let blob_id = store.blob_id_store.lock().unwrap()[&hash].clone();
let mut conn = connection.lock().await;
conn.connect().await?;
let download_url = conn.session.lock().unwrap().download_url.clone();
let mut res = conn
.client
.get_async(&download_request_format(
&conn.session,
conn.mail_account_id(),
download_url.as_str(),
&conn.mail_account_id(),
&blob_id,
None,
))
@ -75,9 +74,9 @@ impl BackendOp for JmapOp {
let res_text = res.text_async().await?;
store
.write()
.unwrap()
.byte_cache
.lock()
.unwrap()
.entry(hash)
.or_default()
.bytes = Some(res_text.clone());

View File

@ -23,13 +23,8 @@ use super::mailbox::JmapMailbox;
use super::*;
use serde::Serialize;
use serde_json::{json, Value};
use smallvec::SmallVec;
use std::collections::hash_map::DefaultHasher;
use std::collections::HashMap;
use std::convert::TryFrom;
use std::hash::{Hash, Hasher};
use std::convert::{TryFrom, TryInto};
pub type Id = String;
pub type UtcDate = String;
use super::rfc8620::Object;
@ -43,19 +38,6 @@ macro_rules! get_request_no {
}};
}
macro_rules! tag_hash {
($t:ident) => {{
let mut hasher = DefaultHasher::default();
$t.hash(&mut hasher);
hasher.finish()
}};
($t:literal) => {{
let mut hasher = DefaultHasher::default();
$t.hash(&mut hasher);
hasher.finish()
}};
}
pub trait Response<OBJ: Object> {
const NAME: &'static str;
}
@ -104,10 +86,11 @@ pub struct JsonResponse<'a> {
pub async fn get_mailboxes(conn: &JmapConnection) -> Result<HashMap<MailboxHash, JmapMailbox>> {
let seq = get_request_no!(conn.request_no);
let api_url = conn.session.lock().unwrap().api_url.clone();
let mut res = conn
.client
.post_async(
&conn.session.api_url,
api_url.as_str(),
serde_json::to_string(&json!({
"using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
"methodCalls": [["Mailbox/get", {
@ -120,13 +103,13 @@ pub async fn get_mailboxes(conn: &JmapConnection) -> Result<HashMap<MailboxHash,
let res_text = res.text_async().await?;
let mut v: MethodResponse = serde_json::from_str(&res_text).unwrap();
*conn.online_status.lock().await = (std::time::Instant::now(), Ok(()));
*conn.store.online_status.lock().await = (std::time::Instant::now(), Ok(()));
let m = GetResponse::<MailboxObject>::try_from(v.method_responses.remove(0))?;
let GetResponse::<MailboxObject> {
list, account_id, ..
} = m;
*conn.account_id.lock().unwrap() = account_id;
Ok(list
*conn.store.account_id.lock().unwrap() = account_id;
let mut ret: HashMap<MailboxHash, JmapMailbox> = list
.into_iter()
.map(|r| {
let MailboxObject {
@ -142,18 +125,26 @@ pub async fn get_mailboxes(conn: &JmapConnection) -> Result<HashMap<MailboxHash,
unread_emails,
unread_threads,
} = r;
let hash = crate::get_path_hash!(&name);
let mut total_emails_set = LazyCountSet::default();
total_emails_set.set_not_yet_seen(total_emails.try_into().unwrap_or(0));
let total_emails = total_emails_set;
let mut unread_emails_set = LazyCountSet::default();
unread_emails_set.set_not_yet_seen(unread_emails.try_into().unwrap_or(0));
let unread_emails = unread_emails_set;
let hash = id.into_hash();
let parent_hash = parent_id.clone().map(|id| id.into_hash());
(
hash,
JmapMailbox {
name: name.clone(),
hash,
path: name,
v: Vec::new(),
children: Vec::new(),
id,
is_subscribed,
my_rights,
parent_id,
parent_hash,
role,
usage: Default::default(),
sort_order,
@ -161,16 +152,27 @@ pub async fn get_mailboxes(conn: &JmapConnection) -> Result<HashMap<MailboxHash,
total_threads,
unread_emails: Arc::new(Mutex::new(unread_emails)),
unread_threads,
email_state: Arc::new(Mutex::new(None)),
email_query_state: Arc::new(Mutex::new(None)),
},
)
})
.collect())
.collect();
for key in ret.keys().cloned().collect::<SmallVec<[MailboxHash; 24]>>() {
if let Some(parent_hash) = ret[&key].parent_hash.clone() {
ret.entry(parent_hash).and_modify(|e| e.children.push(key));
}
}
Ok(ret)
}
pub async fn get_message_list(conn: &JmapConnection, mailbox: &JmapMailbox) -> Result<Vec<String>> {
pub async fn get_message_list(
conn: &JmapConnection,
mailbox: &JmapMailbox,
) -> Result<Vec<Id<EmailObject>>> {
let email_call: EmailQuery = EmailQuery::new(
Query::new()
.account_id(conn.mail_account_id().to_string())
.account_id(conn.mail_account_id().clone())
.filter(Some(Filter::Condition(
EmailFilterCondition::new()
.in_mailbox(Some(mailbox.id.clone()))
@ -183,19 +185,21 @@ pub async fn get_message_list(conn: &JmapConnection, mailbox: &JmapMailbox) -> R
let mut req = Request::new(conn.request_no.clone());
req.add_call(&email_call);
let api_url = conn.session.lock().unwrap().api_url.clone();
let mut res = conn
.client
.post_async(&conn.session.api_url, serde_json::to_string(&req)?)
.post_async(api_url.as_str(), serde_json::to_string(&req)?)
.await?;
let res_text = res.text_async().await?;
let mut v: MethodResponse = serde_json::from_str(&res_text).unwrap();
*conn.online_status.lock().await = (std::time::Instant::now(), Ok(()));
*conn.store.online_status.lock().await = (std::time::Instant::now(), Ok(()));
let m = QueryResponse::<EmailObject>::try_from(v.method_responses.remove(0))?;
let QueryResponse::<EmailObject> { ids, .. } = m;
Ok(ids)
}
/*
pub async fn get_message(conn: &JmapConnection, ids: &[String]) -> Result<Vec<Envelope>> {
let email_call: EmailGet = EmailGet::new(
Get::new()
@ -219,18 +223,17 @@ pub async fn get_message(conn: &JmapConnection, ids: &[String]) -> Result<Vec<En
.map(std::convert::Into::into)
.collect::<Vec<Envelope>>())
}
*/
pub async fn fetch(
conn: &JmapConnection,
store: &Arc<RwLock<Store>>,
tag_index: &Arc<RwLock<BTreeMap<u64, String>>>,
mailboxes: &Arc<RwLock<HashMap<MailboxHash, JmapMailbox>>>,
store: &Store,
mailbox_hash: MailboxHash,
) -> Result<Vec<Envelope>> {
let mailbox_id = mailboxes.read().unwrap()[&mailbox_hash].id.clone();
let mailbox_id = store.mailboxes.read().unwrap()[&mailbox_hash].id.clone();
let email_query_call: EmailQuery = EmailQuery::new(
Query::new()
.account_id(conn.mail_account_id().to_string())
.account_id(conn.mail_account_id().clone())
.filter(Some(Filter::Condition(
EmailFilterCondition::new()
.in_mailbox(Some(mailbox_id))
@ -249,93 +252,74 @@ pub async fn fetch(
prev_seq,
EmailQuery::RESULT_FIELD_IDS,
)))
.account_id(conn.mail_account_id().to_string()),
.account_id(conn.mail_account_id().clone()),
);
req.add_call(&email_call);
let api_url = conn.session.lock().unwrap().api_url.clone();
let mut res = conn
.client
.post_async(&conn.session.api_url, serde_json::to_string(&req)?)
.post_async(api_url.as_str(), serde_json::to_string(&req)?)
.await?;
let res_text = res.text_async().await?;
let mut v: MethodResponse = serde_json::from_str(&res_text).unwrap();
let e = GetResponse::<EmailObject>::try_from(v.method_responses.pop().unwrap())?;
let query_response = QueryResponse::<EmailObject>::try_from(v.method_responses.pop().unwrap())?;
store
.mailboxes
.write()
.unwrap()
.entry(mailbox_hash)
.and_modify(|mbox| {
*mbox.email_query_state.lock().unwrap() = Some(query_response.query_state);
});
let GetResponse::<EmailObject> { list, state, .. } = e;
{
let mut states_lck = conn.method_call_states.lock().unwrap();
if let Some(prev_state) = states_lck.get_mut(&EmailGet::NAME) {
debug!("{:?}: prev_state was {}", EmailGet::NAME, prev_state);
if *prev_state != state { /* FIXME Query Changes. */ }
*prev_state = state;
debug!("{:?}: curr state is {}", EmailGet::NAME, prev_state);
} else {
debug!("{:?}: inserting state {}", EmailGet::NAME, &state);
states_lck.insert(EmailGet::NAME, state);
}
}
let mut tag_lck = tag_index.write().unwrap();
let ids = list
.iter()
.map(|obj| {
let tags = obj
.keywords()
.keys()
.map(|tag| {
let tag_hash = {
let mut hasher = DefaultHasher::default();
tag.hash(&mut hasher);
hasher.finish()
};
if !tag_lck.contains_key(&tag_hash) {
tag_lck.insert(tag_hash, tag.to_string());
}
tag_hash
let (is_empty, is_equal) = {
let mailboxes_lck = conn.store.mailboxes.read().unwrap();
mailboxes_lck
.get(&mailbox_hash)
.map(|mbox| {
let current_state_lck = mbox.email_state.lock().unwrap();
(
current_state_lck.is_none(),
current_state_lck.as_ref() != Some(&state),
)
})
.collect::<SmallVec<[u64; 1024]>>();
(tags, obj.id.clone(), obj.blob_id.clone())
})
.collect::<Vec<(SmallVec<[u64; 1024]>, Id, Id)>>();
drop(tag_lck);
let mut ret = list
.into_iter()
.map(std::convert::Into::into)
.collect::<Vec<Envelope>>();
let mut store_lck = store.write().unwrap();
debug_assert_eq!(tag_hash!("$draft"), 6613915297903591176);
debug_assert_eq!(tag_hash!("$seen"), 1683863812294339685);
debug_assert_eq!(tag_hash!("$flagged"), 2714010747478170100);
debug_assert_eq!(tag_hash!("$answered"), 8940855303929342213);
debug_assert_eq!(tag_hash!("$junk"), 2656839745430720464);
debug_assert_eq!(tag_hash!("$notjunk"), 4091323799684325059);
for (env, (tags, id, blob_id)) in ret.iter_mut().zip(ids.into_iter()) {
store_lck.id_store.insert(env.hash(), id);
store_lck.blob_id_store.insert(env.hash(), blob_id);
for t in tags {
match t {
6613915297903591176 => {
env.set_flags(env.flags() | Flag::DRAFT);
}
1683863812294339685 => {
env.set_flags(env.flags() | Flag::SEEN);
}
2714010747478170100 => {
env.set_flags(env.flags() | Flag::FLAGGED);
}
8940855303929342213 => {
env.set_flags(env.flags() | Flag::REPLIED);
}
2656839745430720464 | 4091323799684325059 => { /* ignore */ }
_ => env.labels_mut().push(t),
}
.unwrap_or((true, true))
};
if is_empty {
let mut mailboxes_lck = conn.store.mailboxes.write().unwrap();
debug!("{:?}: inserting state {}", EmailObject::NAME, &state);
mailboxes_lck.entry(mailbox_hash).and_modify(|mbox| {
*mbox.email_state.lock().unwrap() = Some(state);
});
} else if !is_equal {
conn.email_changes(mailbox_hash).await?;
}
}
let mut total = BTreeSet::default();
let mut unread = BTreeSet::default();
let mut ret = Vec::with_capacity(list.len());
for obj in list {
let env = store.add_envelope(obj);
total.insert(env.hash());
if !env.is_seen() {
unread.insert(env.hash());
}
ret.push(env);
}
let mut mailboxes_lck = store.mailboxes.write().unwrap();
mailboxes_lck.entry(mailbox_hash).and_modify(|mbox| {
mbox.total_emails.lock().unwrap().insert_existing_set(total);
mbox.unread_emails
.lock()
.unwrap()
.insert_existing_set(unread);
});
Ok(ret)
}

View File

@ -19,12 +19,13 @@
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
use super::Id;
use crate::email::parser::BytesExt;
use core::marker::PhantomData;
use serde::de::DeserializeOwned;
use serde::ser::{Serialize, SerializeStruct, Serializer};
use serde_json::{value::RawValue, Value};
use std::hash::{Hash, Hasher};
use std::sync::Arc;
mod filters;
pub use filters::*;
@ -39,23 +40,189 @@ pub trait Object {
const NAME: &'static str;
}
#[derive(Deserialize, Serialize)]
#[serde(transparent)]
pub struct Id<OBJ> {
pub inner: String,
#[serde(skip)]
pub _ph: PhantomData<fn() -> OBJ>,
}
impl<OBJ: Object> core::fmt::Debug for Id<OBJ> {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.debug_tuple(&format!("Id<{}>", OBJ::NAME))
.field(&self.inner)
.finish()
}
}
impl core::fmt::Debug for Id<String> {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.debug_tuple("Id<Any>").field(&self.inner).finish()
}
}
//, Hash, Eq, PartialEq, Default)]
impl<OBJ> Clone for Id<OBJ> {
fn clone(&self) -> Self {
Id {
inner: self.inner.clone(),
_ph: PhantomData,
}
}
}
impl<OBJ> std::cmp::Eq for Id<OBJ> {}
impl<OBJ> std::cmp::PartialEq for Id<OBJ> {
fn eq(&self, other: &Self) -> bool {
self.inner == other.inner
}
}
impl<OBJ> Hash for Id<OBJ> {
fn hash<H: Hasher>(&self, state: &mut H) {
self.inner.hash(state);
}
}
impl<OBJ> Default for Id<OBJ> {
fn default() -> Self {
Self::new()
}
}
impl<OBJ> From<String> for Id<OBJ> {
fn from(inner: String) -> Self {
Id {
inner,
_ph: PhantomData,
}
}
}
impl<OBJ> core::fmt::Display for Id<OBJ> {
fn fmt(&self, fmt: &mut core::fmt::Formatter) -> core::fmt::Result {
core::fmt::Display::fmt(&self.inner, fmt)
}
}
impl<OBJ> Id<OBJ> {
pub fn new() -> Self {
Self {
inner: String::new(),
_ph: PhantomData,
}
}
pub fn as_str(&self) -> &str {
self.inner.as_str()
}
pub fn len(&self) -> usize {
self.inner.len()
}
pub fn is_empty(&self) -> bool {
self.inner.is_empty()
}
}
#[derive(Deserialize, Serialize, Debug)]
#[serde(transparent)]
pub struct State<OBJ> {
pub inner: String,
#[serde(skip)]
pub _ph: PhantomData<fn() -> OBJ>,
}
//, Hash, Eq, PartialEq, Default)]
impl<OBJ> Clone for State<OBJ> {
fn clone(&self) -> Self {
State {
inner: self.inner.clone(),
_ph: PhantomData,
}
}
}
impl<OBJ> std::cmp::Eq for State<OBJ> {}
impl<OBJ> std::cmp::PartialEq for State<OBJ> {
fn eq(&self, other: &Self) -> bool {
self.inner == other.inner
}
}
impl<OBJ> Hash for State<OBJ> {
fn hash<H: Hasher>(&self, state: &mut H) {
self.inner.hash(state);
}
}
impl<OBJ> Default for State<OBJ> {
fn default() -> Self {
Self::new()
}
}
impl<OBJ> From<String> for State<OBJ> {
fn from(inner: String) -> Self {
State {
inner,
_ph: PhantomData,
}
}
}
impl<OBJ> core::fmt::Display for State<OBJ> {
fn fmt(&self, fmt: &mut core::fmt::Formatter) -> core::fmt::Result {
core::fmt::Display::fmt(&self.inner, fmt)
}
}
impl<OBJ> State<OBJ> {
pub fn new() -> Self {
Self {
inner: String::new(),
_ph: PhantomData,
}
}
pub fn as_str(&self) -> &str {
self.inner.as_str()
}
pub fn len(&self) -> usize {
self.inner.len()
}
pub fn is_empty(&self) -> bool {
self.inner.is_empty()
}
}
#[derive(Deserialize, Serialize, Debug, Default)]
#[serde(rename_all = "camelCase")]
pub struct JmapSession {
pub capabilities: HashMap<String, CapabilitiesObject>,
pub accounts: HashMap<Id, Account>,
pub primary_accounts: HashMap<String, Id>,
pub accounts: HashMap<Id<Account>, Account>,
pub primary_accounts: HashMap<String, Id<Account>>,
pub username: String,
pub api_url: String,
pub download_url: String,
pub api_url: Arc<String>,
pub download_url: Arc<String>,
pub upload_url: String,
pub event_source_url: String,
pub state: String,
pub upload_url: Arc<String>,
pub event_source_url: Arc<String>,
pub state: State<JmapSession>,
#[serde(flatten)]
pub extra_properties: HashMap<String, Value>,
}
impl Object for JmapSession {
const NAME: &'static str = "Session";
}
#[derive(Deserialize, Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct CapabilitiesObject {
@ -88,6 +255,17 @@ pub struct Account {
extra_properties: HashMap<String, Value>,
}
impl Object for Account {
const NAME: &'static str = "Account";
}
#[derive(Debug)]
pub struct BlobObject;
impl Object for BlobObject {
const NAME: &'static str = "Blob";
}
/// #`get`
///
/// Objects of type `Foo` are fetched via a call to `Foo/get`.
@ -104,11 +282,10 @@ pub struct Get<OBJ: Object>
where
OBJ: std::fmt::Debug + Serialize,
{
#[serde(skip_serializing_if = "String::is_empty")]
pub account_id: String,
pub account_id: Id<Account>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(flatten)]
pub ids: Option<JmapArgument<Vec<String>>>,
pub ids: Option<JmapArgument<Vec<Id<OBJ>>>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub properties: Option<Vec<String>>,
#[serde(skip)]
@ -121,7 +298,7 @@ where
{
pub fn new() -> Self {
Self {
account_id: String::new(),
account_id: Id::new(),
ids: None,
properties: None,
_ph: PhantomData,
@ -132,7 +309,7 @@ where
///
/// The id of the account to use.
///
account_id: String
account_id: Id<Account>
);
_impl!(
/// - ids: `Option<JmapArgument<Vec<String>>>`
@ -142,7 +319,7 @@ where
/// type and the number of records does not exceed the
/// "max_objects_in_get" limit.
///
ids: Option<JmapArgument<Vec<String>>>
ids: Option<JmapArgument<Vec<Id<OBJ>>>>
);
_impl!(
/// - properties: Option<Vec<String>>
@ -218,20 +395,19 @@ pub struct MethodResponse<'a> {
#[serde(borrow)]
pub method_responses: Vec<&'a RawValue>,
#[serde(default)]
pub created_ids: HashMap<Id, Id>,
pub created_ids: HashMap<Id<String>, Id<String>>,
#[serde(default)]
pub session_state: String,
pub session_state: State<JmapSession>,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct GetResponse<OBJ: Object> {
#[serde(skip_serializing_if = "String::is_empty")]
pub account_id: String,
#[serde(default)]
pub state: String,
pub account_id: Id<Account>,
#[serde(default = "State::default")]
pub state: State<OBJ>,
pub list: Vec<OBJ>,
pub not_found: Vec<String>,
pub not_found: Vec<Id<OBJ>>,
}
impl<OBJ: Object + DeserializeOwned> std::convert::TryFrom<&RawValue> for GetResponse<OBJ> {
@ -244,10 +420,10 @@ impl<OBJ: Object + DeserializeOwned> std::convert::TryFrom<&RawValue> for GetRes
}
impl<OBJ: Object> GetResponse<OBJ> {
_impl!(get_mut account_id_mut, account_id: String);
_impl!(get_mut state_mut, state: String);
_impl!(get_mut account_id_mut, account_id: Id<Account>);
_impl!(get_mut state_mut, state: State<OBJ>);
_impl!(get_mut list_mut, list: Vec<OBJ>);
_impl!(get_mut not_found_mut, not_found: Vec<String>);
_impl!(get_mut not_found_mut, not_found: Vec<Id<OBJ>>);
}
#[derive(Deserialize, Debug)]
@ -264,7 +440,7 @@ pub struct Query<F: FilterTrait<OBJ>, OBJ: Object>
where
OBJ: std::fmt::Debug + Serialize,
{
account_id: String,
account_id: Id<Account>,
filter: Option<F>,
sort: Option<Comparator<OBJ>>,
#[serde(default)]
@ -288,7 +464,7 @@ where
{
pub fn new() -> Self {
Self {
account_id: String::new(),
account_id: Id::new(),
filter: None,
sort: None,
position: 0,
@ -300,7 +476,7 @@ where
}
}
_impl!(account_id: String);
_impl!(account_id: Id<Account>);
_impl!(filter: Option<F>);
_impl!(sort: Option<Comparator<OBJ>>);
_impl!(position: u64);
@ -325,12 +501,11 @@ pub fn bool_true() -> bool {
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct QueryResponse<OBJ: Object> {
#[serde(skip_serializing_if = "String::is_empty", default)]
pub account_id: String,
pub account_id: Id<Account>,
pub query_state: String,
pub can_calculate_changes: bool,
pub position: u64,
pub ids: Vec<Id>,
pub ids: Vec<Id<OBJ>>,
#[serde(default)]
pub total: u64,
#[serde(default)]
@ -349,7 +524,7 @@ impl<OBJ: Object + DeserializeOwned> std::convert::TryFrom<&RawValue> for QueryR
}
impl<OBJ: Object> QueryResponse<OBJ> {
_impl!(get_mut ids_mut, ids: Vec<Id>);
_impl!(get_mut ids_mut, ids: Vec<Id<OBJ>>);
}
pub struct ResultField<M: Method<OBJ>, OBJ: Object> {
@ -410,9 +585,8 @@ pub struct Changes<OBJ: Object>
where
OBJ: std::fmt::Debug + Serialize,
{
#[serde(skip_serializing_if = "String::is_empty")]
pub account_id: String,
pub since_state: String,
pub account_id: Id<Account>,
pub since_state: State<OBJ>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_changes: Option<u64>,
#[serde(skip)]
@ -425,8 +599,8 @@ where
{
pub fn new() -> Self {
Self {
account_id: String::new(),
since_state: String::new(),
account_id: Id::new(),
since_state: State::new(),
max_changes: None,
_ph: PhantomData,
}
@ -436,7 +610,7 @@ where
///
/// The id of the account to use.
///
account_id: String
account_id: Id<Account>
);
_impl!(
/// - since_state: "String"
@ -446,7 +620,7 @@ where
/// state.
///
///
since_state: String
since_state: State<OBJ>
);
_impl!(
/// - max_changes: "UnsignedInt|null"
@ -463,16 +637,15 @@ where
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct ChangesResponse<OBJ: Object> {
#[serde(skip_serializing_if = "String::is_empty")]
pub account_id: String,
pub old_state: String,
pub new_state: String,
pub account_id: Id<Account>,
pub old_state: State<OBJ>,
pub new_state: State<OBJ>,
pub has_more_changes: bool,
pub created: Vec<Id>,
pub updated: Vec<Id>,
pub destroyed: Vec<Id>,
pub created: Vec<Id<OBJ>>,
pub updated: Vec<Id<OBJ>>,
pub destroyed: Vec<Id<OBJ>>,
#[serde(skip)]
_ph: PhantomData<fn() -> OBJ>,
pub _ph: PhantomData<fn() -> OBJ>,
}
impl<OBJ: Object + DeserializeOwned> std::convert::TryFrom<&RawValue> for ChangesResponse<OBJ> {
@ -485,13 +658,13 @@ impl<OBJ: Object + DeserializeOwned> std::convert::TryFrom<&RawValue> for Change
}
impl<OBJ: Object> ChangesResponse<OBJ> {
_impl!(get_mut account_id_mut, account_id: String);
_impl!(get_mut old_state_mut, old_state: String);
_impl!(get_mut new_state_mut, new_state: String);
_impl!(get_mut account_id_mut, account_id: Id<Account>);
_impl!(get_mut old_state_mut, old_state: State<OBJ>);
_impl!(get_mut new_state_mut, new_state: State<OBJ>);
_impl!(get has_more_changes, has_more_changes: bool);
_impl!(get_mut created_mut, created: Vec<String>);
_impl!(get_mut updated_mut, updated: Vec<String>);
_impl!(get_mut destroyed_mut, destroyed: Vec<String>);
_impl!(get_mut created_mut, created: Vec<Id<OBJ>>);
_impl!(get_mut updated_mut, updated: Vec<Id<OBJ>>);
_impl!(get_mut destroyed_mut, destroyed: Vec<Id<OBJ>>);
}
///#`set`
@ -508,11 +681,10 @@ pub struct Set<OBJ: Object>
where
OBJ: std::fmt::Debug + Serialize,
{
#[serde(skip_serializing_if = "String::is_empty")]
///o accountId: "Id"
///
/// The id of the account to use.
pub account_id: String,
pub account_id: Id<Account>,
///o ifInState: "String|null"
///
/// This is a state string as returned by the "Foo/get" method
@ -521,7 +693,7 @@ where
/// otherwise, the method will be aborted and a "stateMismatch" error
/// returned. If null, any changes will be applied to the current
/// state.
pub if_in_state: Option<String>,
pub if_in_state: Option<State<OBJ>>,
///o create: "Id[Foo]|null"
///
/// A map of a *creation id* (a temporary id set by the client) to Foo
@ -533,7 +705,7 @@ where
/// The client MUST omit any properties that may only be set by the
/// server (for example, the "id" property on most object types).
///
pub create: Option<HashMap<Id, OBJ>>,
pub create: Option<HashMap<Id<OBJ>, OBJ>>,
///o update: "Id[PatchObject]|null"
///
/// A map of an id to a Patch object to apply to the current Foo
@ -577,12 +749,12 @@ where
/// is also a valid PatchObject. The client may choose to optimise
/// network usage by just sending the diff or may send the whole
/// object; the server processes it the same either way.
pub update: Option<HashMap<Id, Value>>,
pub update: Option<HashMap<Id<OBJ>, Value>>,
///o destroy: "Id[]|null"
///
/// A list of ids for Foo objects to permanently delete, or null if no
/// objects are to be destroyed.
pub destroy: Option<Vec<Id>>,
pub destroy: Option<Vec<Id<OBJ>>>,
}
impl<OBJ: Object> Set<OBJ>
@ -591,14 +763,14 @@ where
{
pub fn new() -> Self {
Self {
account_id: String::new(),
account_id: Id::new(),
if_in_state: None,
create: None,
update: None,
destroy: None,
}
}
_impl!(account_id: String);
_impl!(account_id: Id<Account>);
_impl!(
///o ifInState: "String|null"
///
@ -608,9 +780,9 @@ where
/// otherwise, the method will be aborted and a "stateMismatch" error
/// returned. If null, any changes will be applied to the current
/// state.
if_in_state: Option<String>
if_in_state: Option<State<OBJ>>
);
_impl!(update: Option<HashMap<Id, Value>>);
_impl!(update: Option<HashMap<Id<OBJ>, Value>>);
}
#[derive(Serialize, Deserialize, Debug)]
@ -619,17 +791,17 @@ pub struct SetResponse<OBJ: Object> {
///o accountId: "Id"
///
/// The id of the account used for the call.
pub account_id: String,
pub account_id: Id<Account>,
///o oldState: "String|null"
///
/// The state string that would have been returned by "Foo/get" before
/// making the requested changes, or null if the server doesn't know
/// what the previous state string was.
pub old_state: String,
pub old_state: State<OBJ>,
///o newState: "String"
///
/// The state string that will now be returned by "Foo/get".
pub new_state: String,
pub new_state: State<OBJ>,
///o created: "Id[Foo]|null"
///
/// A map of the creation id to an object containing any properties of
@ -639,7 +811,7 @@ pub struct SetResponse<OBJ: Object> {
/// and thus set to a default by the server.
///
/// This argument is null if no Foo objects were successfully created.
pub created: Option<HashMap<Id, OBJ>>,
pub created: Option<HashMap<Id<OBJ>, OBJ>>,
///o updated: "Id[Foo|null]|null"
///
/// The keys in this map are the ids of all Foos that were
@ -651,12 +823,12 @@ pub struct SetResponse<OBJ: Object> {
/// any changes to server-set or computed properties.
///
/// This argument is null if no Foo objects were successfully updated.
pub updated: Option<HashMap<Id, Option<OBJ>>>,
pub updated: Option<HashMap<Id<OBJ>, Option<OBJ>>>,
///o destroyed: "Id[]|null"
///
/// A list of Foo ids for records that were successfully destroyed, or
/// null if none.
pub destroyed: Option<Vec<Id>>,
pub destroyed: Option<Vec<Id<OBJ>>>,
///o notCreated: "Id[SetError]|null"
///
/// A map of the creation id to a SetError object for each record that
@ -759,50 +931,50 @@ impl core::fmt::Display for SetError {
}
pub fn download_request_format(
session: &JmapSession,
account_id: &Id,
blob_id: &Id,
download_url: &str,
account_id: &Id<Account>,
blob_id: &Id<BlobObject>,
name: Option<String>,
) -> String {
// https://jmap.fastmail.com/download/{accountId}/{blobId}/{name}
let mut ret = String::with_capacity(
session.download_url.len()
download_url.len()
+ blob_id.len()
+ name.as_ref().map(|n| n.len()).unwrap_or(0)
+ account_id.len(),
);
let mut prev_pos = 0;
while let Some(pos) = session.download_url.as_bytes()[prev_pos..].find(b"{") {
ret.push_str(&session.download_url[prev_pos..prev_pos + pos]);
while let Some(pos) = download_url.as_bytes()[prev_pos..].find(b"{") {
ret.push_str(&download_url[prev_pos..prev_pos + pos]);
prev_pos += pos;
if session.download_url[prev_pos..].starts_with("{accountId}") {
ret.push_str(account_id);
if download_url[prev_pos..].starts_with("{accountId}") {
ret.push_str(account_id.as_str());
prev_pos += "{accountId}".len();
} else if session.download_url[prev_pos..].starts_with("{blobId}") {
ret.push_str(blob_id);
} else if download_url[prev_pos..].starts_with("{blobId}") {
ret.push_str(blob_id.as_str());
prev_pos += "{blobId}".len();
} else if session.download_url[prev_pos..].starts_with("{name}") {
} else if download_url[prev_pos..].starts_with("{name}") {
ret.push_str(name.as_ref().map(String::as_str).unwrap_or(""));
prev_pos += "{name}".len();
}
}
if prev_pos != session.download_url.len() {
ret.push_str(&session.download_url[prev_pos..]);
if prev_pos != download_url.len() {
ret.push_str(&download_url[prev_pos..]);
}
ret
}
pub fn upload_request_format(session: &JmapSession, account_id: &Id) -> String {
pub fn upload_request_format(upload_url: &str, account_id: &Id<Account>) -> String {
//"uploadUrl": "https://jmap.fastmail.com/upload/{accountId}/",
let mut ret = String::with_capacity(session.upload_url.len() + account_id.len());
let mut ret = String::with_capacity(upload_url.len() + account_id.len());
let mut prev_pos = 0;
while let Some(pos) = session.upload_url.as_bytes()[prev_pos..].find(b"{") {
ret.push_str(&session.upload_url[prev_pos..prev_pos + pos]);
while let Some(pos) = upload_url.as_bytes()[prev_pos..].find(b"{") {
ret.push_str(&upload_url[prev_pos..prev_pos + pos]);
prev_pos += pos;
if session.upload_url[prev_pos..].starts_with("{accountId}") {
ret.push_str(account_id);
if upload_url[prev_pos..].starts_with("{accountId}") {
ret.push_str(account_id.as_str());
prev_pos += "{accountId}".len();
break;
} else {
@ -810,8 +982,193 @@ pub fn upload_request_format(session: &JmapSession, account_id: &Id) -> String {
prev_pos += 1;
}
}
if prev_pos != session.upload_url.len() {
ret.push_str(&session.upload_url[prev_pos..]);
if prev_pos != upload_url.len() {
ret.push_str(&upload_url[prev_pos..]);
}
ret
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct UploadResponse {
///o accountId: "Id"
///
/// The id of the account used for the call.
pub account_id: Id<Account>,
///o blobId: "Id"
///
///The id representing the binary data uploaded. The data for this id is immutable.
///The id *only* refers to the binary data, not any metadata.
pub blob_id: Id<BlobObject>,
///o type: "String"
///
///The media type of the file (as specified in [RFC6838],
///Section 4.2) as set in the Content-Type header of the upload HTTP
///request.
#[serde(rename = "type")]
pub _type: String,
///o size: "UnsignedInt"
///
/// The size of the file in octets.
pub size: usize,
}
/// #`queryChanges`
///
/// The "Foo/queryChanges" method allows a client to efficiently update
/// the state of a cached query to match the new state on the server. It
/// takes the following arguments:
#[derive(Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct QueryChanges<F: FilterTrait<OBJ>, OBJ: Object>
where
OBJ: std::fmt::Debug + Serialize,
{
pub account_id: Id<Account>,
pub filter: Option<F>,
pub sort: Option<Comparator<OBJ>>,
///sinceQueryState: "String"
///
///The current state of the query in the client. This is the string
///that was returned as the "queryState" argument in the "Foo/query"
///response with the same sort/filter. The server will return the
///changes made to the query since this state.
pub since_query_state: String,
///o maxChanges: "UnsignedInt|null"
///
///The maximum number of changes to return in the response. See
///error descriptions below for more details.
pub max_changes: Option<usize>,
///o upToId: "Id|null"
///
///The last (highest-index) id the client currently has cached from
///the query results. When there are a large number of results, in a
///common case, the client may have only downloaded and cached a
///small subset from the beginning of the results. If the sort and
///filter are both only on immutable properties, this allows the
///server to omit changes after this point in the results, which can
///significantly increase efficiency. If they are not immutable,
///this argument is ignored.
pub up_to_id: Option<Id<OBJ>>,
///o calculateTotal: "Boolean" (default: false)
///
///Does the client wish to know the total number of results now in
///the query? This may be slow and expensive for servers to
///calculate, particularly with complex filters, so clients should
///take care to only request the total when needed.
#[serde(default = "bool_false")]
pub calculate_total: bool,
#[serde(skip)]
_ph: PhantomData<fn() -> OBJ>,
}
impl<F: FilterTrait<OBJ>, OBJ: Object> QueryChanges<F, OBJ>
where
OBJ: std::fmt::Debug + Serialize,
{
pub fn new(account_id: Id<Account>, since_query_state: String) -> Self {
Self {
account_id,
filter: None,
sort: None,
since_query_state,
max_changes: None,
up_to_id: None,
calculate_total: false,
_ph: PhantomData,
}
}
_impl!(filter: Option<F>);
_impl!(sort: Option<Comparator<OBJ>>);
_impl!(max_changes: Option<usize>);
_impl!(up_to_id: Option<Id<OBJ>>);
_impl!(calculate_total: bool);
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct QueryChangesResponse<OBJ: Object> {
/// The id of the account used for the call.
pub account_id: Id<Account>,
/// This is the "sinceQueryState" argument echoed back; that is, the state from which the server is returning changes.
pub old_query_state: String,
///This is the state the query will be in after applying the set of changes to the old state.
pub new_query_state: String,
/// The total number of Foos in the results (given the "filter"). This argument MUST be omitted if the "calculateTotal" request argument is not true.
#[serde(default)]
pub total: Option<usize>,
///The "id" for every Foo that was in the query results in the old
///state and that is not in the results in the new state.
///If the server cannot calculate this exactly, the server MAY return
///the ids of extra Foos in addition that may have been in the old
///results but are not in the new results.
///If the sort and filter are both only on immutable properties and
///an "upToId" is supplied and exists in the results, any ids that
///were removed but have a higher index than "upToId" SHOULD be
///omitted.
///If the "filter" or "sort" includes a mutable property, the server
///MUST include all Foos in the current results for which this
///property may have changed. The position of these may have moved
///in the results, so they must be reinserted by the client to ensure
///its query cache is correct.
pub removed: Vec<Id<OBJ>>,
///The id and index in the query results (in the new state) for every
///Foo that has been added to the results since the old state AND
///every Foo in the current results that was included in the
///"removed" array (due to a filter or sort based upon a mutable
///property).
///If the sort and filter are both only on immutable properties and
///an "upToId" is supplied and exists in the results, any ids that
///were added but have a higher index than "upToId" SHOULD be
///omitted.
///The array MUST be sorted in order of index, with the lowest index
///first.
///An *AddedItem* object has the following properties:
///* id: "Id"
///* index: "UnsignedInt"
///The result of this is that if the client has a cached sparse array of
///Foo ids corresponding to the results in the old state, then:
///fooIds = [ "id1", "id2", null, null, "id3", "id4", null, null, null ]
///If it *splices out* all ids in the removed array that it has in its
///cached results, then:
/// removed = [ "id2", "id31", ... ];
/// fooIds => [ "id1", null, null, "id3", "id4", null, null, null ]
///and *splices in* (one by one in order, starting with the lowest
///index) all of the ids in the added array:
///added = [{ id: "id5", index: 0, ... }];
///fooIds => [ "id5", "id1", null, null, "id3", "id4", null, null, null ]
///and *truncates* or *extends* to the new total length, then the
///results will now be in the new state.
///Note: splicing in adds the item at the given index, incrementing the
///index of all items previously at that or a higher index. Splicing
///out is the inverse, removing the item and decrementing the index of
///every item after it in the array.
pub added: Vec<AddedItem<OBJ>>,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct AddedItem<OBJ: Object> {
pub id: Id<OBJ>,
pub index: usize,
}

View File

@ -30,12 +30,11 @@ use crate::backends::*;
use crate::email::Flag;
use crate::error::{MeliError, Result};
use crate::shellexpand::ShellExpandTrait;
pub use futures::stream::Stream;
use memmap::{Mmap, Protection};
use futures::stream::Stream;
use std::collections::hash_map::DefaultHasher;
use std::fs;
use std::hash::{Hash, Hasher};
use std::io::{BufReader, Read};
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
@ -45,7 +44,7 @@ pub struct MaildirOp {
hash_index: HashIndexes,
mailbox_hash: MailboxHash,
hash: EnvelopeHash,
slice: Option<Mmap>,
slice: Option<Vec<u8>>,
}
impl Clone for MaildirOp {
@ -68,7 +67,7 @@ impl MaildirOp {
slice: None,
}
}
fn path(&self) -> PathBuf {
fn path(&self) -> Result<PathBuf> {
let map = self.hash_index.lock().unwrap();
let map = &map[&self.mailbox_hash];
debug!("looking for {} in {} map", self.hash, self.mailbox_hash);
@ -77,30 +76,38 @@ impl MaildirOp {
for e in map.iter() {
debug!("{:#?}", e);
}
return Err(MeliError::new("File not found"));
}
if let Some(modif) = &map[&self.hash].modified {
Ok(if let Some(modif) = &map[&self.hash].modified {
match modif {
PathMod::Path(ref path) => path.clone(),
PathMod::Hash(hash) => map[&hash].to_path_buf(),
}
} else {
map.get(&self.hash).unwrap().to_path_buf()
}
})
}
}
impl<'a> BackendOp for MaildirOp {
fn as_bytes(&mut self) -> ResultFuture<Vec<u8>> {
if self.slice.is_none() {
self.slice = Some(Mmap::open_path(self.path(), Protection::Read)?);
let file = std::fs::OpenOptions::new()
.read(true)
.write(false)
.open(&self.path()?)?;
let mut buf_reader = BufReader::new(file);
let mut contents = Vec::new();
buf_reader.read_to_end(&mut contents)?;
self.slice = Some(contents);
}
/* Unwrap is safe since we use ? above. */
let ret = Ok((unsafe { self.slice.as_ref().unwrap().as_slice() }).to_vec());
let ret = Ok(self.slice.as_ref().unwrap().as_slice().to_vec());
Ok(Box::pin(async move { ret }))
}
fn fetch_flags(&self) -> ResultFuture<Flag> {
let path = self.path();
let path = self.path()?;
let ret = Ok(path.flags());
Ok(Box::pin(async move { ret }))
}

View File

@ -25,9 +25,9 @@ use crate::conf::AccountSettings;
use crate::email::{Envelope, EnvelopeHash, Flag};
use crate::error::{ErrorKind, MeliError, Result};
use crate::shellexpand::ShellExpandTrait;
use crate::Collection;
use futures::prelude::Stream;
use memmap::{Mmap, Protection};
extern crate notify;
use self::notify::{watcher, DebouncedEvent, RecursiveMode, Watcher};
use std::time::Duration;
@ -110,6 +110,7 @@ pub struct MaildirType {
mailbox_index: Arc<Mutex<HashMap<EnvelopeHash, MailboxHash>>>,
hash_indexes: HashIndexes,
event_consumer: BackendEventConsumer,
collection: Collection,
path: PathBuf,
}
@ -142,15 +143,6 @@ macro_rules! get_path_hash {
}
pub(super) fn get_file_hash(file: &Path) -> EnvelopeHash {
/*
let mut buf = Vec::with_capacity(2048);
let mut f = fs::File::open(&file).unwrap_or_else(|_| panic!("Can't open {}", file.display()));
f.read_to_end(&mut buf)
.unwrap_or_else(|_| panic!("Can't read {}", file.display()));
let mut hasher = DefaultHasher::default();
hasher.write(&buf);
hasher.finish()
*/
let mut hasher = DefaultHasher::default();
file.hash(&mut hasher);
hasher.finish()
@ -241,6 +233,7 @@ impl MailBackend for MaildirType {
Ok(Box::pin(async move {
let thunk = move |sender: &BackendEventConsumer| {
debug!("refreshing");
let mut buf = Vec::with_capacity(4096);
let mut path = path.clone();
path.push("new");
for d in path.read_dir()? {
@ -278,10 +271,10 @@ impl MailBackend for MaildirType {
}
(*map).insert(hash, PathBuf::from(&file).into());
}
if let Ok(mut env) = Envelope::from_bytes(
unsafe { &Mmap::open_path(&file, Protection::Read)?.as_slice() },
Some(file.flags()),
) {
let mut reader = io::BufReader::new(fs::File::open(&file)?);
buf.clear();
reader.read_to_end(&mut buf)?;
if let Ok(mut env) = Envelope::from_bytes(buf.as_slice(), Some(file.flags())) {
env.set_hash(hash);
mailbox_index
.lock()
@ -298,7 +291,11 @@ impl MailBackend for MaildirType {
f.set_permissions(permissions)?;
let writer = io::BufWriter::new(f);
bincode::serialize_into(writer, &env)?;
bincode::Options::serialize_into(
bincode::config::DefaultOptions::new(),
writer,
&env,
)?;
}
(sender)(
account_hash,
@ -371,6 +368,7 @@ impl MailBackend for MaildirType {
Ok(Box::pin(async move {
// Move `watcher` in the closure's scope so that it doesn't get dropped.
let _watcher = watcher;
let mut buf = Vec::with_capacity(4096);
loop {
match rx.recv() {
/*
@ -410,6 +408,7 @@ impl MailBackend for MaildirType {
pathbuf.as_path(),
&cache_dir,
file_name,
&mut buf,
) {
mailbox_index
.lock()
@ -464,6 +463,7 @@ impl MailBackend for MaildirType {
pathbuf.as_path(),
&cache_dir,
file_name,
&mut buf,
) {
mailbox_index
.lock()
@ -482,14 +482,14 @@ impl MailBackend for MaildirType {
}
};
let new_hash: EnvelopeHash = get_file_hash(pathbuf.as_path());
let mut reader = io::BufReader::new(fs::File::open(&pathbuf)?);
buf.clear();
reader.read_to_end(&mut buf)?;
if index_lock.get_mut(&new_hash).is_none() {
debug!("write notice");
if let Ok(mut env) = Envelope::from_bytes(
unsafe {
&Mmap::open_path(&pathbuf, Protection::Read)?.as_slice()
},
Some(pathbuf.flags()),
) {
if let Ok(mut env) =
Envelope::from_bytes(buf.as_slice(), Some(pathbuf.flags()))
{
env.set_hash(new_hash);
debug!("{}\t{:?}", new_hash, &pathbuf);
debug!(
@ -543,9 +543,13 @@ impl MailBackend for MaildirType {
});
continue;
}
*mailbox_counts[&mailbox_hash].1.lock().unwrap() -= 1;
{
let mut lck = mailbox_counts[&mailbox_hash].1.lock().unwrap();
*lck = lck.saturating_sub(1);
}
if !pathbuf.flags().contains(Flag::SEEN) {
*mailbox_counts[&mailbox_hash].0.lock().unwrap() -= 1;
let mut lck = mailbox_counts[&mailbox_hash].0.lock().unwrap();
*lck = lck.saturating_sub(1);
}
index_lock.entry(hash).and_modify(|e| {
@ -611,6 +615,7 @@ impl MailBackend for MaildirType {
dest.as_path(),
&cache_dir,
file_name,
&mut buf,
) {
mailbox_index
.lock()
@ -701,6 +706,7 @@ impl MailBackend for MaildirType {
dest.as_path(),
&cache_dir,
file_name,
&mut buf,
) {
mailbox_index
.lock()
@ -747,6 +753,7 @@ impl MailBackend for MaildirType {
dest.as_path(),
&cache_dir,
file_name,
&mut buf,
) {
mailbox_index
.lock()
@ -913,6 +920,37 @@ impl MailBackend for MaildirType {
}))
}
fn delete_messages(
&mut self,
env_hashes: EnvelopeHashBatch,
mailbox_hash: MailboxHash,
) -> ResultFuture<()> {
let hash_index = self.hash_indexes.clone();
Ok(Box::pin(async move {
let mut hash_indexes_lck = hash_index.lock().unwrap();
let hash_index = hash_indexes_lck.entry(mailbox_hash).or_default();
for env_hash in env_hashes.iter() {
let _path = {
if !hash_index.contains_key(&env_hash) {
continue;
}
if let Some(modif) = &hash_index[&env_hash].modified {
match modif {
PathMod::Path(ref path) => path.clone(),
PathMod::Hash(hash) => hash_index[&hash].to_path_buf(),
}
} else {
hash_index[&env_hash].to_path_buf()
}
};
fs::remove_file(&_path)?;
}
Ok(())
}))
}
fn copy_messages(
&mut self,
env_hashes: EnvelopeHashBatch,
@ -967,6 +1005,10 @@ impl MailBackend for MaildirType {
}))
}
fn collection(&self) -> Collection {
self.collection.clone()
}
fn create_mailbox(
&mut self,
new_path: String,
@ -1200,6 +1242,7 @@ impl MaildirType {
hash_indexes: Arc::new(Mutex::new(hash_indexes)),
mailbox_index: Default::default(),
event_consumer,
collection: Default::default(),
path: root_path,
}))
}
@ -1299,6 +1342,7 @@ fn add_path_to_index(
path: &Path,
cache_dir: &xdg::BaseDirectories,
file_name: PathBuf,
buf: &mut Vec<u8>,
) -> Result<Envelope> {
debug!("add_path_to_index path {:?} filename{:?}", path, file_name);
let env_hash = get_file_hash(path);
@ -1313,11 +1357,10 @@ fn add_path_to_index(
map.len()
);
}
//Mmap::open_path(self.path(), Protection::Read)?
let mut env = Envelope::from_bytes(
unsafe { &Mmap::open_path(path, Protection::Read)?.as_slice() },
Some(path.flags()),
)?;
let mut reader = io::BufReader::new(fs::File::open(&path)?);
buf.clear();
reader.read_to_end(buf)?;
let mut env = Envelope::from_bytes(buf.as_slice(), Some(path.flags()))?;
env.set_hash(env_hash);
debug!(
"add_path_to_index gen {}\t{}",
@ -1334,7 +1377,7 @@ fn add_path_to_index(
permissions.set_mode(0o600); // Read/write for owner only.
f.set_permissions(permissions)?;
let writer = io::BufWriter::new(f);
bincode::serialize_into(writer, &env)?;
bincode::Options::serialize_into(bincode::config::DefaultOptions::new(), writer, &env)?;
}
Ok(env)
}

View File

@ -25,8 +25,7 @@ use core::future::Future;
use core::pin::Pin;
use futures::stream::{FuturesUnordered, StreamExt};
use futures::task::{Context, Poll};
use memmap::{Mmap, Protection};
use std::io::{self};
use std::io::{self, Read};
use std::os::unix::fs::PermissionsExt;
use std::path::PathBuf;
use std::result;
@ -51,6 +50,7 @@ impl MaildirStream {
map: HashIndexes,
mailbox_index: Arc<Mutex<HashMap<EnvelopeHash, MailboxHash>>>,
) -> Result<Pin<Box<dyn Stream<Item = Result<Vec<Envelope>>> + Send + 'static>>> {
let chunk_size = 2048;
path.push("new");
for d in path.read_dir()? {
if let Ok(p) = d {
@ -58,7 +58,6 @@ impl MaildirStream {
}
}
path.pop();
path.push("cur");
let iter = path.read_dir()?;
let count = path.read_dir()?.count();
@ -71,16 +70,9 @@ impl MaildirStream {
files.push(e);
}
let payloads = Box::pin(if !files.is_empty() {
let cores = 4_usize;
let chunk_size = if count / cores > 0 {
count / cores
} else {
count
};
files
.chunks(chunk_size)
.map(|chunk| {
//Self::chunk(chunk, name, mailbox_hash, unseen, total, path, root_path, map, mailbox_index)})
let cache_dir = xdg::BaseDirectories::with_profile("meli", &name).unwrap();
Box::pin(Self::chunk(
SmallVec::from(chunk),
@ -110,79 +102,90 @@ impl MaildirStream {
map: HashIndexes,
mailbox_index: Arc<Mutex<HashMap<EnvelopeHash, MailboxHash>>>,
) -> Result<Vec<Envelope>> {
let len = chunk.len();
let size = if len <= 100 { 100 } else { (len / 100) * 100 };
let mut local_r: Vec<Envelope> = Vec::with_capacity(chunk.len());
for c in chunk.chunks(size) {
let map = map.clone();
for file in c {
/* Check if we have a cache file with this email's
* filename */
let file_name = PathBuf::from(file)
.strip_prefix(&root_path)
.unwrap()
.to_path_buf();
if let Some(cached) = cache_dir.find_cache_file(&file_name) {
/* Cached struct exists, try to load it */
let reader = io::BufReader::new(fs::File::open(&cached).unwrap());
let result: result::Result<Envelope, _> = bincode::deserialize_from(reader);
if let Ok(env) = result {
let mut map = map.lock().unwrap();
let map = map.entry(mailbox_hash).or_default();
let hash = env.hash();
map.insert(hash, file.clone().into());
mailbox_index.lock().unwrap().insert(hash, mailbox_hash);
if !env.is_seen() {
*unseen.lock().unwrap() += 1;
}
*total.lock().unwrap() += 1;
local_r.push(env);
continue;
}
};
let env_hash = get_file_hash(file);
{
let mut unseen_total: usize = 0;
let mut buf = Vec::with_capacity(4096);
for file in chunk {
/* Check if we have a cache file with this email's
* filename */
let file_name = PathBuf::from(&file)
.strip_prefix(&root_path)
.unwrap()
.to_path_buf();
if let Some(cached) = cache_dir.find_cache_file(&file_name) {
/* Cached struct exists, try to load it */
let cached_file = fs::File::open(&cached)?;
let filesize = cached_file.metadata()?.len();
let reader = io::BufReader::new(cached_file);
let result: result::Result<Envelope, _> = bincode::Options::deserialize_from(
bincode::Options::with_limit(
bincode::config::DefaultOptions::new(),
2 * filesize,
),
reader,
);
if let Ok(env) = result {
let mut map = map.lock().unwrap();
let map = map.entry(mailbox_hash).or_default();
(*map).insert(env_hash, PathBuf::from(file).into());
let hash = env.hash();
map.insert(hash, file.clone().into());
mailbox_index.lock().unwrap().insert(hash, mailbox_hash);
if !env.is_seen() {
unseen_total += 1;
}
local_r.push(env);
continue;
}
match Envelope::from_bytes(
unsafe { &Mmap::open_path(&file, Protection::Read)?.as_slice() },
Some(file.flags()),
) {
Ok(mut env) => {
env.set_hash(env_hash);
mailbox_index.lock().unwrap().insert(env_hash, mailbox_hash);
if let Ok(cached) = cache_dir.place_cache_file(file_name) {
/* place result in cache directory */
let f = fs::File::create(cached)?;
let metadata = f.metadata()?;
let mut permissions = metadata.permissions();
/* Try delete invalid file */
let _ = fs::remove_file(&cached);
};
let env_hash = get_file_hash(&file);
{
let mut map = map.lock().unwrap();
let map = map.entry(mailbox_hash).or_default();
map.insert(env_hash, PathBuf::from(&file).into());
}
let mut reader = io::BufReader::new(fs::File::open(&file)?);
buf.clear();
reader.read_to_end(&mut buf)?;
match Envelope::from_bytes(buf.as_slice(), Some(file.flags())) {
Ok(mut env) => {
env.set_hash(env_hash);
mailbox_index.lock().unwrap().insert(env_hash, mailbox_hash);
if let Ok(cached) = cache_dir.place_cache_file(file_name) {
/* place result in cache directory */
let f = fs::File::create(cached)?;
let metadata = f.metadata()?;
let mut permissions = metadata.permissions();
permissions.set_mode(0o600); // Read/write for owner only.
f.set_permissions(permissions)?;
permissions.set_mode(0o600); // Read/write for owner only.
f.set_permissions(permissions)?;
let writer = io::BufWriter::new(f);
bincode::serialize_into(writer, &env)?;
}
if !env.is_seen() {
*unseen.lock().unwrap() += 1;
}
*total.lock().unwrap() += 1;
local_r.push(env);
let writer = io::BufWriter::new(f);
bincode::Options::serialize_into(
bincode::config::DefaultOptions::new(),
writer,
&env,
)?;
}
Err(err) => {
debug!(
"DEBUG: hash {}, path: {} couldn't be parsed, {}",
env_hash,
file.as_path().display(),
err,
);
continue;
if !env.is_seen() {
unseen_total += 1;
}
local_r.push(env);
}
Err(err) => {
debug!(
"DEBUG: hash {}, path: {} couldn't be parsed, {}",
env_hash,
file.as_path().display(),
err,
);
continue;
}
}
}
*total.lock().unwrap() += local_r.len();
*unseen.lock().unwrap() += unseen_total;
Ok(local_r)
}
}
@ -190,7 +193,6 @@ impl MaildirStream {
impl Stream for MaildirStream {
type Item = Result<Vec<Envelope>>;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
//todo!()
let payloads = self.payloads.as_mut();
payloads.poll_next(cx)
}

View File

@ -19,18 +19,115 @@
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
/*!
* https://wiki2.dovecot.org/MailboxFormat/mbox
*/
//! # Mbox formats
//!
//! ## Resources
//!
//! - [0] <https://web.archive.org/web/20160812091518/https://jdebp.eu./FGA/mail-mbox-formats.html>
//! - [1] <https://wiki2.dovecot.org/MailboxFormat/mbox>
//! - [2] <https://manpages.debian.org/buster/mutt/mbox.5.en.html>
//!
//! ## `mbox` format
//! `mbox` describes a family of incompatible legacy formats.
//!
//! "All of the 'mbox' formats store all of the messages in the mailbox in a single file. Delivery appends new messages to the end of the file." [0]
//!
//! "Each message is preceded by a From_ line and followed by a blank line. A From_ line is a line that begins with the five characters 'F', 'r', 'o', 'm', and ' '." [0]
//!
//! ## `From ` / postmark line
//!
//! "An mbox is a text file containing an arbitrary number of e-mail messages. Each message
//! consists of a postmark, followed by an e-mail message formatted according to RFC822, RFC2822.
//! The file format is line-oriented. Lines are separated by line feed characters (ASCII 10).
//!
//! "A postmark line consists of the four characters 'From', followed by a space character,
//! followed by the message's envelope sender address, followed by whitespace, and followed by a
//! time stamp. This line is often called From_ line.
//!
//! "The sender address is expected to be addr-spec as defined in RFC2822 3.4.1. The date is expected
//! to be date-time as output by asctime(3). For compatibility reasons with legacy software,
//! two-digit years greater than or equal to 70 should be interpreted as the years 1970+, while
//! two-digit years less than 70 should be interpreted as the years 2000-2069. Software reading
//! files in this format should also be prepared to accept non-numeric timezone information such as
//! 'CET DST' for Central European Time, daylight saving time.
//!
//! "Example:
//!
//!```text
//!From example@example.com Fri Jun 23 02:56:55 2000
//!```
//!
//! "In order to avoid misinterpretation of lines in message bodies which begin with the four
//! characters 'From', followed by a space character, the mail delivery agent must quote
//! any occurrence of 'From ' at the start of a body line." [2]
//!
//! ## Metadata
//!
//! `melib` recognizes the CClient (a [Pine client API](https://web.archive.org/web/20050203003235/http://www.washington.edu/imap/)) convention for metadata in `mbox` format:
//!
//! - `Status`: R (Seen) and O (non-Recent) flags
//! - `X-Status`: A (Answered), F (Flagged), T (Draft) and D (Deleted) flags
//! - `X-Keywords`: Messages keywords
//!
//! ## Parsing an mbox file
//!
//! ```
//! # use melib::{Result, Envelope, EnvelopeHash, mbox::*};
//! # use std::collections::HashMap;
//! # use std::sync::{Arc, Mutex};
//! let file_contents = vec![]; // Replace with actual mbox file contents
//! let index: Arc<Mutex<HashMap<EnvelopeHash, (Offset, Length)>>> = Arc::new(Mutex::new(HashMap::default()));
//! let mut message_iter = MessageIterator {
//! index: index.clone(),
//! input: &file_contents.as_slice(),
//! offset: 0,
//! file_offset: 0,
//! format: Some(MboxFormat::MboxCl2),
//! };
//! let envelopes: Result<Vec<Envelope>> = message_iter.collect();
//! ```
//!
//! ## Writing / Appending an mbox file
//!
//! ```no_run
//! # use melib::mbox::*;
//! # use std::io::Write;
//! let mbox_1: &[u8] = br#"From: <a@b.c>\n\nHello World"#;
//! let mbox_2: &[u8] = br#"From: <d@e.f>\n\nHello World #2"#;
//! let mut file = std::io::BufWriter::new(std::fs::File::create(&"out.mbox")?);
//! let format = MboxFormat::MboxCl2;
//! format.append(
//! &mut file,
//! mbox_1,
//! None, // Envelope From
//! Some(melib::datetime::now()), // Delivered date
//! Default::default(), // Flags and tags
//! MboxMetadata::None,
//! true,
//! false,
//! )?;
//! format.append(
//! &mut file,
//! mbox_2,
//! None,
//! Some(melib::datetime::now()),
//! Default::default(), // Flags and tags
//! MboxMetadata::None,
//! false,
//! false,
//! )?;
//! file.flush()?;
//! # Ok::<(), melib::MeliError>(())
//! ```
use crate::backends::*;
use crate::collection::Collection;
use crate::conf::AccountSettings;
use crate::email::parser::BytesExt;
use crate::email::*;
use crate::error::{MeliError, Result};
use crate::get_path_hash;
use crate::shellexpand::ShellExpandTrait;
use memmap::{Mmap, Protection};
use nom::bytes::complete::tag;
use nom::character::complete::digit1;
use nom::combinator::map_res;
@ -41,22 +138,24 @@ use self::notify::{watcher, DebouncedEvent, RecursiveMode, Watcher};
use std::collections::hash_map::{DefaultHasher, HashMap};
use std::fs::File;
use std::hash::Hasher;
use std::io::BufReader;
use std::io::Read;
use std::io::{BufReader, Read};
use std::os::unix::io::AsRawFd;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::sync::mpsc::channel;
use std::sync::{Arc, Mutex, RwLock};
type Offset = usize;
type Length = usize;
pub mod write;
pub type Offset = usize;
pub type Length = usize;
#[cfg(target_os = "linux")]
const F_OFD_SETLKW: libc::c_int = 38;
// Open file description locking
// # man fcntl
fn get_rw_lock_blocking(f: &File) {
fn get_rw_lock_blocking(f: &File, path: &Path) -> Result<()> {
let fd: libc::c_int = f.as_raw_fd();
let mut flock: libc::flock = libc::flock {
l_type: libc::F_WRLCK as libc::c_short,
@ -65,11 +164,23 @@ fn get_rw_lock_blocking(f: &File) {
l_len: 0, /* "Specifying 0 for l_len has the special meaning: lock all bytes starting at the location
specified by l_whence and l_start through to the end of file, no matter how large the file grows." */
l_pid: 0, /* "By contrast with traditional record locks, the l_pid field of that structure must be set to zero when using the commands described below." */
#[cfg(target_os = "freebsd")]
l_sysid: 0,
};
let ptr: *mut libc::flock = &mut flock;
#[cfg(not(target_os = "linux"))]
let ret_val = unsafe { libc::fcntl(fd, libc::F_SETLKW, ptr as *mut libc::c_void) };
#[cfg(target_os = "linux")]
let ret_val = unsafe { libc::fcntl(fd, F_OFD_SETLKW, ptr as *mut libc::c_void) };
debug!(&ret_val);
assert!(-1 != ret_val);
if ret_val == -1 {
let err = nix::errno::Errno::from_i32(nix::errno::errno());
return Err(MeliError::new(format!(
"Could not lock {}: fcntl() returned {}",
path.display(),
err.desc()
)));
}
Ok(())
}
#[derive(Debug)]
@ -164,7 +275,7 @@ pub struct MboxOp {
path: PathBuf,
offset: Offset,
length: Length,
slice: Option<Mmap>,
slice: std::cell::RefCell<Option<Vec<u8>>>,
}
impl MboxOp {
@ -172,7 +283,7 @@ impl MboxOp {
MboxOp {
hash,
path: path.to_path_buf(),
slice: None,
slice: std::cell::RefCell::new(None),
offset,
length,
}
@ -181,28 +292,38 @@ impl MboxOp {
impl BackendOp for MboxOp {
fn as_bytes(&mut self) -> ResultFuture<Vec<u8>> {
if self.slice.is_none() {
self.slice = Some(Mmap::open_path(&self.path, Protection::Read)?);
if self.slice.get_mut().is_none() {
let file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open(&self.path)?;
get_rw_lock_blocking(&file, &self.path)?;
let mut buf_reader = BufReader::new(file);
let mut contents = Vec::new();
buf_reader.read_to_end(&mut contents)?;
*self.slice.get_mut() = Some(contents);
}
/* Unwrap is safe since we use ? above. */
let ret = Ok((unsafe {
&self.slice.as_ref().unwrap().as_slice()[self.offset..self.offset + self.length]
})
.to_vec());
let ret = Ok(self.slice.get_mut().as_ref().unwrap().as_slice()
[self.offset..self.offset + self.length]
.to_vec());
Ok(Box::pin(async move { ret }))
}
fn fetch_flags(&self) -> ResultFuture<Flag> {
let mut flags = Flag::empty();
let file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open(&self.path)?;
get_rw_lock_blocking(&file);
let mut buf_reader = BufReader::new(file);
let mut contents = Vec::new();
buf_reader.read_to_end(&mut contents)?;
let (_, headers) = parser::headers::headers_raw(contents.as_slice())?;
if self.slice.borrow().is_none() {
let file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open(&self.path)?;
get_rw_lock_blocking(&file, &self.path)?;
let mut buf_reader = BufReader::new(file);
let mut contents = Vec::new();
buf_reader.read_to_end(&mut contents)?;
*self.slice.borrow_mut() = Some(contents);
}
let slice_ref = self.slice.borrow();
let (_, headers) = parser::headers::headers_raw(slice_ref.as_ref().unwrap().as_slice())?;
if let Some(start) = headers.find(b"Status:") {
if let Some(end) = headers[start..].find(b"\n") {
let start = start + b"Status:".len();
@ -250,14 +371,30 @@ impl BackendOp for MboxOp {
}
#[derive(Debug, Clone, Copy)]
pub enum MboxReader {
pub enum MboxMetadata {
/// Dovecot uses C-Client (ie. UW-IMAP, Pine) compatible headers in mbox messages to store me
/// - X-IMAPbase: Contains UIDVALIDITY, last used UID and list of used keywords
/// - X-IMAP: Same as X-IMAPbase but also specifies that the message is a “pseudo message”
/// - X-UID: Messages allocated UID
/// - Status: R (Seen) and O (non-Recent) flags
/// - X-Status: A (Answered), F (Flagged), T (Draft) and D (Deleted) flags
/// - X-Keywords: Messages keywords
/// - Content-Length: Length of the message body in bytes
CClient,
None,
}
/// Choose between "mboxo", "mboxrd", "mboxcl", "mboxcl2". For new mailboxes, prefer "mboxcl2"
/// which does not alter the mail body.
#[derive(Debug, Clone, Copy)]
pub enum MboxFormat {
MboxO,
MboxRd,
MboxCl,
MboxCl2,
}
impl Default for MboxReader {
impl Default for MboxFormat {
fn default() -> Self {
Self::MboxCl2
}
@ -300,8 +437,8 @@ macro_rules! find_From__line {
}};
}
impl MboxReader {
fn parse<'i>(&self, input: &'i [u8]) -> IResult<&'i [u8], Envelope> {
impl MboxFormat {
pub fn parse<'i>(&self, input: &'i [u8]) -> IResult<&'i [u8], Envelope> {
let orig_input = input;
let mut input = input;
match self {
@ -584,7 +721,7 @@ pub fn mbox_parse(
index: Arc<Mutex<HashMap<EnvelopeHash, (Offset, Length)>>>,
input: &[u8],
file_offset: usize,
reader: Option<MboxReader>,
format: Option<MboxFormat>,
) -> IResult<&[u8], Vec<Envelope>> {
if input.is_empty() {
return Err(nom::Err::Error((input, ErrorKind::Tag)));
@ -593,9 +730,9 @@ pub fn mbox_parse(
let mut index = index.lock().unwrap();
let mut envelopes = Vec::with_capacity(32);
let reader = reader.unwrap_or(MboxReader::MboxCl2);
let format = format.unwrap_or(MboxFormat::MboxCl2);
while !input[offset + file_offset..].is_empty() {
let (next_input, env) = match reader.parse(&input[offset + file_offset..]) {
let (next_input, env) = match format.parse(&input[offset + file_offset..]) {
Ok(v) => v,
Err(e) => {
// Try to recover from this error by finding a new candidate From_ line
@ -627,12 +764,12 @@ pub fn mbox_parse(
Ok((&[], envelopes))
}
struct MessageIterator<'a> {
index: Arc<Mutex<HashMap<EnvelopeHash, (Offset, Length)>>>,
input: &'a [u8],
file_offset: usize,
offset: usize,
reader: Option<MboxReader>,
pub struct MessageIterator<'a> {
pub index: Arc<Mutex<HashMap<EnvelopeHash, (Offset, Length)>>>,
pub input: &'a [u8],
pub file_offset: usize,
pub offset: usize,
pub format: Option<MboxFormat>,
}
impl<'a> Iterator for MessageIterator<'a> {
@ -643,10 +780,10 @@ impl<'a> Iterator for MessageIterator<'a> {
}
let mut index = self.index.lock().unwrap();
let reader = self.reader.unwrap_or(MboxReader::MboxCl2);
let format = self.format.unwrap_or(MboxFormat::MboxCl2);
while !self.input[self.offset + self.file_offset..].is_empty() {
let (next_input, env) =
match reader.parse(&self.input[self.offset + self.file_offset..]) {
match format.parse(&self.input[self.offset + self.file_offset..]) {
Ok(v) => v,
Err(e) => {
// Try to recover from this error by finding a new candidate From_ line
@ -687,9 +824,10 @@ impl<'a> Iterator for MessageIterator<'a> {
pub struct MboxType {
account_name: String,
path: PathBuf,
collection: Collection,
mailbox_index: Arc<Mutex<HashMap<EnvelopeHash, MailboxHash>>>,
mailboxes: Arc<Mutex<HashMap<MailboxHash, MboxMailbox>>>,
prefer_mbox_type: Option<MboxReader>,
prefer_mbox_type: Option<MboxFormat>,
event_consumer: BackendEventConsumer,
}
@ -718,7 +856,7 @@ impl MailBackend for MboxType {
mailbox_hash: MailboxHash,
mailbox_index: Arc<Mutex<HashMap<EnvelopeHash, MailboxHash>>>,
mailboxes: Arc<Mutex<HashMap<MailboxHash, MboxMailbox>>>,
prefer_mbox_type: Option<MboxReader>,
prefer_mbox_type: Option<MboxFormat>,
offset: usize,
file_offset: usize,
contents: Vec<u8>,
@ -733,7 +871,7 @@ impl MailBackend for MboxType {
input: &self.contents.as_slice(),
offset: self.offset,
file_offset: self.file_offset,
reader: self.prefer_mbox_type,
format: self.prefer_mbox_type,
};
let mut payload = vec![];
let mut done = false;
@ -782,7 +920,7 @@ impl MailBackend for MboxType {
.read(true)
.write(true)
.open(&mailbox_path)?;
get_rw_lock_blocking(&file);
get_rw_lock_blocking(&file, &mailbox_path)?;
let mut buf_reader = BufReader::new(file);
let mut contents = Vec::new();
buf_reader.read_to_end(&mut contents)?;
@ -860,7 +998,7 @@ impl MailBackend for MboxType {
continue;
}
};
get_rw_lock_blocking(&file);
get_rw_lock_blocking(&file, &pathbuf)?;
let mut mailbox_lock = mailboxes.lock().unwrap();
let mut buf_reader = BufReader::new(file);
let mut contents = Vec::new();
@ -1012,6 +1150,14 @@ impl MailBackend for MboxType {
Err(MeliError::new("Unimplemented."))
}
fn delete_messages(
&mut self,
_env_hashes: EnvelopeHashBatch,
_mailbox_hash: MailboxHash,
) -> ResultFuture<()> {
Err(MeliError::new("Unimplemented."))
}
fn save(
&self,
_bytes: Vec<u8>,
@ -1028,6 +1174,10 @@ impl MailBackend for MboxType {
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn collection(&self) -> Collection {
self.collection.clone()
}
}
macro_rules! get_conf_val {
@ -1079,10 +1229,10 @@ impl MboxType {
path,
prefer_mbox_type: match prefer_mbox_type.as_str() {
"auto" => None,
"mboxo" => Some(MboxReader::MboxO),
"mboxrd" => Some(MboxReader::MboxRd),
"mboxcl" => Some(MboxReader::MboxCl),
"mboxcl2" => Some(MboxReader::MboxCl2),
"mboxo" => Some(MboxFormat::MboxO),
"mboxrd" => Some(MboxFormat::MboxRd),
"mboxcl" => Some(MboxFormat::MboxCl),
"mboxcl2" => Some(MboxFormat::MboxCl2),
_ => {
return Err(MeliError::new(format!(
"{} invalid `prefer_mbox_type` value: `{}`",
@ -1091,6 +1241,7 @@ impl MboxType {
)))
}
},
collection: Collection::default(),
mailbox_index: Default::default(),
mailboxes: Default::default(),
};

View File

@ -0,0 +1,260 @@
/*
* meli - mailbox module.
*
* Copyright 2021 Manos Pitsidianakis
*
* This file is part of meli.
*
* meli is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* meli is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
use super::*;
impl MboxFormat {
pub fn append(
&self,
writer: &mut dyn std::io::Write,
input: &[u8],
envelope_from: Option<&Address>,
delivery_date: Option<crate::UnixTimestamp>,
(flags, tags): (Flag, Vec<&str>),
metadata_format: MboxMetadata,
is_empty: bool,
crlf: bool,
) -> Result<()> {
if tags.iter().any(|t| t.contains(' ')) {
return Err(MeliError::new("mbox tags/keywords can't contain spaces"));
}
let line_ending: &'static [u8] = if crlf { &b"\r\n"[..] } else { &b"\n"[..] };
if !is_empty {
writer.write_all(line_ending)?;
writer.write_all(line_ending)?;
}
writer.write_all(&b"From "[..])?;
if let Some(from) = envelope_from {
writer.write_all(from.address_spec_raw())?;
} else {
writer.write_all(&b"MAILER-DAEMON"[..])?;
}
writer.write_all(&b" "[..])?;
writer.write_all(
crate::datetime::timestamp_to_string(
delivery_date.unwrap_or_else(|| crate::datetime::now()),
Some(crate::datetime::ASCTIME_FMT),
true,
)
.trim()
.as_bytes(),
)?;
writer.write_all(line_ending)?;
let (mut headers, body) = parser::mail(input)?;
headers.retain(|(header_name, _)| {
!header_name.eq_ignore_ascii_case(b"Status")
&& !header_name.eq_ignore_ascii_case(b"X-Status")
&& !header_name.eq_ignore_ascii_case(b"X-Keywords")
&& !header_name.eq_ignore_ascii_case(b"Content-Length")
});
let write_header_val_fn = |writer: &mut dyn std::io::Write, bytes: &[u8]| {
let mut i = 0;
if crlf {
while i < bytes.len() {
if bytes[i..].starts_with(b"\r\n") {
writer.write_all(&[b'\r', b'\n'])?;
i += 2;
continue;
} else if bytes[i] == b'\n' {
writer.write_all(&[b'\r', b'\n'])?;
} else {
writer.write_all(&[bytes[i]])?;
}
i += 1;
}
} else {
while i < bytes.len() {
if bytes[i..].starts_with(b"\r\n") {
writer.write_all(&[b'\n'])?;
i += 2;
} else {
writer.write_all(&[bytes[i]])?;
i += 1;
}
}
}
Ok::<(), MeliError>(())
};
let write_metadata_fn = |writer: &mut dyn std::io::Write| match metadata_format {
MboxMetadata::CClient => {
for (h, v) in {
if flags.is_seen() {
Some((&b"Status"[..], "R".into()))
} else {
None
}
.into_iter()
.chain(
if !flags.is_flagged()
&& !flags.is_replied()
&& !flags.is_draft()
&& !flags.is_trashed()
{
None
} else {
Some((
&b"X-Status"[..],
format!(
"{flagged}{replied}{draft}{trashed}",
flagged = if flags.is_flagged() { "F" } else { "" },
replied = if flags.is_replied() { "A" } else { "" },
draft = if flags.is_draft() { "T" } else { "" },
trashed = if flags.is_trashed() { "D" } else { "" }
),
))
},
)
.chain(if tags.is_empty() {
None
} else {
Some((&b"X-Keywords"[..], tags.as_slice().join(" ")))
})
} {
writer.write_all(h)?;
writer.write_all(&b": "[..])?;
writer.write_all(v.as_bytes())?;
writer.write_all(line_ending)?;
}
Ok::<(), MeliError>(())
}
MboxMetadata::None => Ok(()),
};
let body_len = {
let mut len = body.len();
if crlf {
let stray_lfs = body.iter().filter(|b| **b == b'\n').count()
- body.windows(b"\r\n".len()).filter(|w| w == b"\r\n").count();
len += stray_lfs;
} else {
let crlfs = body.windows(b"\r\n".len()).filter(|w| w == b"\r\n").count();
len -= crlfs;
}
len
};
match self {
MboxFormat::MboxO | MboxFormat::MboxRd => Err(MeliError::new("Unimplemented.")),
MboxFormat::MboxCl => {
let len = (body_len
+ body
.windows(b"\nFrom ".len())
.filter(|w| w == b"\nFrom ")
.count()
+ if body.starts_with(b"From ") { 1 } else { 0 })
.to_string();
for (h, v) in headers
.into_iter()
.chain(Some((&b"Content-Length"[..], len.as_bytes())))
{
writer.write_all(h)?;
writer.write_all(&b": "[..])?;
write_header_val_fn(writer, v)?;
writer.write_all(line_ending)?;
}
write_metadata_fn(writer)?;
writer.write_all(line_ending)?;
if body.starts_with(b"From ") {
writer.write_all(&[b'>'])?;
}
let mut i = 0;
if crlf {
while i < body.len() {
if body[i..].starts_with(b"\r\n") {
writer.write_all(&[b'\r', b'\n'])?;
if body[i..].starts_with(b"\r\nFrom ") {
writer.write_all(&[b'>'])?;
}
i += 2;
} else if body[i] == b'\n' {
writer.write_all(&[b'\r', b'\n'])?;
if body[i..].starts_with(b"\nFrom ") {
writer.write_all(&[b'>'])?;
}
i += 1;
} else {
writer.write_all(&[body[i]])?;
i += 1;
}
}
} else {
while i < body.len() {
if body[i..].starts_with(b"\r\n") {
writer.write_all(&[b'\n'])?;
if body[i..].starts_with(b"\r\nFrom ") {
writer.write_all(&[b'>'])?;
}
i += 2;
} else {
writer.write_all(&[body[i]])?;
if body[i..].starts_with(b"\nFrom ") {
writer.write_all(&[b'>'])?;
}
i += 1;
}
}
}
Ok(())
}
MboxFormat::MboxCl2 => {
let len = body_len.to_string();
for (h, v) in headers
.into_iter()
.chain(Some((&b"Content-Length"[..], len.as_bytes())))
{
writer.write_all(h)?;
writer.write_all(&b": "[..])?;
write_header_val_fn(writer, v)?;
writer.write_all(line_ending)?;
}
write_metadata_fn(writer)?;
writer.write_all(line_ending)?;
let mut i = 0;
if crlf {
while i < body.len() {
if body[i..].starts_with(b"\r\n") {
writer.write_all(&[b'\r', b'\n'])?;
i += 2;
continue;
} else if body[i] == b'\n' {
writer.write_all(&[b'\r', b'\n'])?;
} else {
writer.write_all(&[body[i]])?;
}
i += 1;
}
} else {
while i < body.len() {
if body[i..].starts_with(b"\r\n") {
writer.write_all(&[b'\n'])?;
i += 2;
} else {
writer.write_all(&[body[i]])?;
i += 1;
}
}
}
Ok(())
}
}
}
}

View File

@ -32,19 +32,19 @@ pub use operations::*;
mod connection;
pub use connection::*;
use crate::backends::*;
use crate::conf::AccountSettings;
use crate::connections::timeout;
use crate::email::*;
use crate::error::{MeliError, Result, ResultIntoMeliError};
use crate::{backends::*, Collection};
use futures::lock::Mutex as FutureMutex;
use futures::stream::Stream;
use std::collections::{hash_map::DefaultHasher, BTreeMap, BTreeSet, HashMap, HashSet};
use std::future::Future;
use std::collections::{hash_map::DefaultHasher, BTreeSet, HashMap, HashSet};
use std::hash::Hasher;
use std::pin::Pin;
use std::str::FromStr;
use std::sync::{Arc, Mutex, RwLock};
use std::time::Instant;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
pub type UID = usize;
pub static SUPPORTED_CAPABILITIES: &[&str] = &[
@ -66,20 +66,6 @@ pub struct NntpServerConf {
pub extension_use: NntpExtensionUse,
}
pub struct IsSubscribedFn(Box<dyn Fn(&str) -> bool + Send + Sync>);
impl std::fmt::Debug for IsSubscribedFn {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "IsSubscribedFn Box")
}
}
impl std::ops::Deref for IsSubscribedFn {
type Target = Box<dyn Fn(&str) -> bool + Send + Sync>;
fn deref(&self) -> &Box<dyn Fn(&str) -> bool + Send + Sync> {
&self.0
}
}
type Capabilities = HashSet<String>;
#[derive(Debug)]
@ -91,6 +77,7 @@ pub struct UIDStore {
hash_index: Arc<Mutex<HashMap<EnvelopeHash, (UID, MailboxHash)>>>,
uid_index: Arc<Mutex<HashMap<(MailboxHash, UID), EnvelopeHash>>>,
collection: Collection,
mailboxes: Arc<FutureMutex<HashMap<MailboxHash, NntpMailbox>>>,
is_online: Arc<Mutex<(Instant, Result<()>)>>,
event_consumer: BackendEventConsumer,
@ -111,6 +98,7 @@ impl UIDStore {
hash_index: Default::default(),
uid_index: Default::default(),
mailboxes: Arc::new(FutureMutex::new(Default::default())),
collection: Collection::new(),
is_online: Arc::new(Mutex::new((
Instant::now(),
Err(MeliError::new("Account is uninitialised.")),
@ -234,10 +222,11 @@ impl MailBackend for NntpType {
fn is_online(&self) -> ResultFuture<()> {
let connection = self.connection.clone();
Ok(Box::pin(async move {
match timeout(std::time::Duration::from_secs(3), connection.lock()).await {
match timeout(Some(Duration::from_secs(60 * 16)), connection.lock()).await {
Ok(mut conn) => {
debug!("is_online");
match debug!(timeout(std::time::Duration::from_secs(3), conn.connect()).await) {
match debug!(timeout(Some(Duration::from_secs(60 * 16)), conn.connect()).await)
{
Ok(Ok(())) => Ok(()),
Err(err) | Ok(Err(err)) => {
conn.stream = Err(err.clone());
@ -278,7 +267,7 @@ impl MailBackend for NntpType {
_mailbox_hash: MailboxHash,
_flags: Option<Flag>,
) -> ResultFuture<()> {
Err(MeliError::new("Unimplemented."))
Err(MeliError::new("NNTP doesn't support saving."))
}
fn copy_messages(
@ -288,7 +277,7 @@ impl MailBackend for NntpType {
_destination_mailbox_hash: MailboxHash,
_move_: bool,
) -> ResultFuture<()> {
Err(MeliError::new("Unimplemented."))
Err(MeliError::new("NNTP doesn't support copying/moving."))
}
fn set_flags(
@ -297,11 +286,15 @@ impl MailBackend for NntpType {
_mailbox_hash: MailboxHash,
_flags: SmallVec<[(std::result::Result<Flag, String>, bool); 8]>,
) -> ResultFuture<()> {
Err(MeliError::new("Unimplemented."))
Err(MeliError::new("NNTP doesn't support flags."))
}
fn tags(&self) -> Option<Arc<RwLock<BTreeMap<u64, String>>>> {
None
fn delete_messages(
&mut self,
_env_hashes: EnvelopeHashBatch,
_mailbox_hash: MailboxHash,
) -> ResultFuture<()> {
Err(MeliError::new("NNTP doesn't support deletion."))
}
fn as_any(&self) -> &dyn Any {
@ -312,6 +305,10 @@ impl MailBackend for NntpType {
self
}
fn collection(&self) -> Collection {
self.uid_store.collection.clone()
}
fn create_mailbox(
&mut self,
_path: String,
@ -634,15 +631,3 @@ impl FetchState {
Ok(Some(ret))
}
}
use futures::future::{self, Either};
async fn timeout<O>(dur: std::time::Duration, f: impl Future<Output = O>) -> Result<O> {
futures::pin_mut!(f);
match future::select(f, smol::Timer::after(dur)).await {
Either::Left((out, _)) => Ok(out),
Either::Right(_) => {
Err(MeliError::new("Timedout").set_kind(crate::error::ErrorKind::Network))
}
}
}

View File

@ -90,7 +90,7 @@ impl NntpStream {
let stream = {
let addr = lookup_ipv4(path, server_conf.server_port)?;
AsyncWrapper::new(Connection::Tcp(
TcpStream::connect_timeout(&addr, std::time::Duration::new(4, 0))
TcpStream::connect_timeout(&addr, std::time::Duration::new(16, 0))
.chain_err_kind(crate::error::ErrorKind::Network)?,
))
.chain_err_kind(crate::error::ErrorKind::Network)?
@ -130,7 +130,7 @@ impl NntpStream {
.any(|cap| cap.eq_ignore_ascii_case("VERSION 2"))
{
return Err(MeliError::new(format!(
"Could not connect to {}: server is not NNTP compliant",
"Could not connect to {}: server is not NNTP VERSION 2 compliant",
&server_conf.server_hostname
)));
}
@ -190,8 +190,12 @@ impl NntpStream {
ret.stream = AsyncWrapper::new(Connection::Tls(
conn_result.chain_err_kind(crate::error::ErrorKind::Network)?,
))
.chain_err_summary(|| format!("Could not initiate TLS negotiation to {}.", path))
.chain_err_kind(crate::error::ErrorKind::Network)?;
}
} else {
ret.read_response(&mut res, false, &["200 ", "201 "])
.await?;
}
//ret.send_command(
// format!(
@ -201,9 +205,17 @@ impl NntpStream {
// .as_bytes(),
//)
//.await?;
if let Err(err) = ret
.stream
.get_ref()
.set_keepalive(Some(std::time::Duration::new(60 * 9, 0)))
{
crate::log(
format!("Could not set TCP keepalive in NNTP connection: {}", err),
crate::LoggingLevel::WARN,
);
}
ret.read_response(&mut res, false, &["200 ", "201 "])
.await?;
ret.send_command(b"CAPABILITIES").await?;
ret.read_response(&mut res, true, command_to_replycodes("CAPABILITIES"))
.await?;

View File

@ -18,9 +18,8 @@
* You should have received a copy of the GNU General Public License
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
use crate::backends::imap::LazyCountSet;
use crate::backends::{
BackendMailbox, Mailbox, MailboxHash, MailboxPermissions, SpecialUsageMailbox,
BackendMailbox, LazyCountSet, Mailbox, MailboxHash, MailboxPermissions, SpecialUsageMailbox,
};
use crate::error::*;
use std::sync::{Arc, Mutex};

View File

@ -144,15 +144,6 @@ pub fn over_article(input: &str) -> IResult<&str, (UID, Envelope)> {
}
if let Some(references) = references {
{
if let Ok((_, r)) =
crate::email::parser::address::msg_id_list(references.as_bytes())
{
for v in r {
env.push_references(v);
}
}
}
env.set_references(references.as_bytes());
}

View File

@ -19,11 +19,11 @@
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
use crate::backends::*;
use crate::conf::AccountSettings;
use crate::email::{Envelope, EnvelopeHash, Flag};
use crate::error::{MeliError, Result, ResultIntoMeliError};
use crate::error::{MeliError, Result};
use crate::shellexpand::ShellExpandTrait;
use crate::{backends::*, Collection};
use smallvec::SmallVec;
use std::collections::{
hash_map::{DefaultHasher, HashMap},
@ -101,7 +101,7 @@ impl DbConnection {
*self.revision_uuid.read().unwrap(),
new_revision_uuid
);
let query: Query = Query::new(self.lib.clone(), &self, &query_str)?;
let query: Query = Query::new(&self, &query_str)?;
let iter = query.search()?;
let mailbox_index_lck = mailbox_index.write().unwrap();
let mailboxes_lck = mailboxes.read().unwrap();
@ -130,31 +130,25 @@ impl DbConnection {
}
} else {
let message_id = message.msg_id_cstr().to_string_lossy().to_string();
match message.into_envelope(index.clone(), tag_index.clone()) {
Ok(env) => {
for (&mailbox_hash, m) in mailboxes_lck.iter() {
let query_str = format!("{} id:{}", m.query_str.as_str(), &message_id);
let query: Query = Query::new(self.lib.clone(), self, &query_str)?;
if query.count().unwrap_or(0) > 0 {
let mut total_lck = m.total.lock().unwrap();
let mut unseen_lck = m.unseen.lock().unwrap();
*total_lck += 1;
if !env.is_seen() {
*unseen_lck += 1;
}
(event_consumer)(
account_hash,
BackendEvent::Refresh(RefreshEvent {
account_hash,
mailbox_hash,
kind: Create(Box::new(env.clone())),
}),
);
}
let env = message.into_envelope(&index, &tag_index);
for (&mailbox_hash, m) in mailboxes_lck.iter() {
let query_str = format!("{} id:{}", m.query_str.as_str(), &message_id);
let query: Query = Query::new(self, &query_str)?;
if query.count().unwrap_or(0) > 0 {
let mut total_lck = m.total.lock().unwrap();
let mut unseen_lck = m.unseen.lock().unwrap();
*total_lck += 1;
if !env.is_seen() {
*unseen_lck += 1;
}
}
Err(err) => {
debug!("could not parse message {:?}", err);
(event_consumer)(
account_hash,
BackendEvent::Refresh(RefreshEvent {
account_hash,
mailbox_hash,
kind: Create(Box::new(env.clone())),
}),
);
}
}
}
@ -226,9 +220,10 @@ pub struct NotmuchDb {
mailboxes: Arc<RwLock<HashMap<MailboxHash, NotmuchMailbox>>>,
index: Arc<RwLock<HashMap<EnvelopeHash, CString>>>,
mailbox_index: Arc<RwLock<HashMap<EnvelopeHash, SmallVec<[MailboxHash; 16]>>>>,
tag_index: Arc<RwLock<BTreeMap<u64, String>>>,
collection: Collection,
path: PathBuf,
account_name: Arc<String>,
account_hash: AccountHash,
event_consumer: BackendEventConsumer,
save_messages_to: Option<PathBuf>,
}
@ -352,17 +347,23 @@ impl NotmuchDb {
}
}
let account_hash = {
let mut hasher = DefaultHasher::new();
hasher.write(s.name().as_bytes());
hasher.finish()
};
Ok(Box::new(NotmuchDb {
lib,
revision_uuid: Arc::new(RwLock::new(0)),
path,
index: Arc::new(RwLock::new(Default::default())),
mailbox_index: Arc::new(RwLock::new(Default::default())),
tag_index: Arc::new(RwLock::new(Default::default())),
collection: Collection::default(),
mailboxes: Arc::new(RwLock::new(mailboxes)),
save_messages_to: None,
account_name: Arc::new(s.name().to_string()),
account_hash,
event_consumer,
}))
}
@ -474,21 +475,15 @@ impl MailBackend for NotmuchDb {
} else {
continue;
};
match message.into_envelope(self.index.clone(), self.tag_index.clone()) {
Ok(env) => {
mailbox_index_lck
.entry(env.hash())
.or_default()
.push(self.mailbox_hash);
if !env.is_seen() {
unseen_count += 1;
}
ret.push(env);
}
Err(err) => {
debug!("could not parse message {:?}", err);
}
let env = message.into_envelope(&self.index, &self.tag_index);
mailbox_index_lck
.entry(env.hash())
.or_default()
.push(self.mailbox_hash);
if !env.is_seen() {
unseen_count += 1;
}
ret.push(env);
} else {
done = true;
break;
@ -515,13 +510,13 @@ impl MailBackend for NotmuchDb {
)?);
let index = self.index.clone();
let mailbox_index = self.mailbox_index.clone();
let tag_index = self.tag_index.clone();
let tag_index = self.collection.tag_index.clone();
let mailboxes = self.mailboxes.clone();
let v: Vec<CString>;
{
let mailboxes_lck = mailboxes.read().unwrap();
let mailbox = mailboxes_lck.get(&mailbox_hash).unwrap();
let query: Query = Query::new(self.lib.clone(), &database, mailbox.query_str.as_str())?;
let query: Query = Query::new(&database, mailbox.query_str.as_str())?;
{
let mut total_lck = mailbox.total.lock().unwrap();
let mut unseen_lck = mailbox.unseen.lock().unwrap();
@ -556,11 +551,7 @@ impl MailBackend for NotmuchDb {
}
fn refresh(&mut self, _mailbox_hash: MailboxHash) -> ResultFuture<()> {
let account_hash = {
let mut hasher = DefaultHasher::new();
hasher.write(self.account_name.as_bytes());
hasher.finish()
};
let account_hash = self.account_hash;
let mut database = NotmuchDb::new_connection(
self.path.as_path(),
self.revision_uuid.clone(),
@ -570,7 +561,7 @@ impl MailBackend for NotmuchDb {
let mailboxes = self.mailboxes.clone();
let index = self.index.clone();
let mailbox_index = self.mailbox_index.clone();
let tag_index = self.tag_index.clone();
let tag_index = self.collection.tag_index.clone();
let event_consumer = self.event_consumer.clone();
Ok(Box::pin(async move {
let new_revision_uuid = database.get_revision_uuid();
@ -594,18 +585,14 @@ impl MailBackend for NotmuchDb {
extern crate notify;
use notify::{watcher, RecursiveMode, Watcher};
let account_hash = {
let mut hasher = DefaultHasher::new();
hasher.write(self.account_name.as_bytes());
hasher.finish()
};
let account_hash = self.account_hash;
let collection = self.collection.clone();
let lib = self.lib.clone();
let path = self.path.clone();
let revision_uuid = self.revision_uuid.clone();
let mailboxes = self.mailboxes.clone();
let index = self.index.clone();
let mailbox_index = self.mailbox_index.clone();
let tag_index = self.tag_index.clone();
let event_consumer = self.event_consumer.clone();
let (tx, rx) = std::sync::mpsc::channel();
@ -629,7 +616,7 @@ impl MailBackend for NotmuchDb {
mailboxes.clone(),
index.clone(),
mailbox_index.clone(),
tag_index.clone(),
collection.tag_index.clone(),
account_hash.clone(),
event_consumer.clone(),
new_revision_uuid,
@ -664,7 +651,7 @@ impl MailBackend for NotmuchDb {
hash,
index: self.index.clone(),
bytes: None,
tag_index: self.tag_index.clone(),
collection: self.collection.clone(),
}))
}
@ -706,7 +693,7 @@ impl MailBackend for NotmuchDb {
self.lib.clone(),
true,
)?;
let tag_index = self.tag_index.clone();
let collection = self.collection.clone();
let index = self.index.clone();
Ok(Box::pin(async move {
@ -794,7 +781,11 @@ impl MailBackend for NotmuchDb {
for (f, v) in flags.iter() {
if let (Err(tag), true) = (f, v) {
let hash = tag_hash!(tag);
tag_index.write().unwrap().insert(hash, tag.to_string());
collection
.tag_index
.write()
.unwrap()
.insert(hash, tag.to_string());
}
}
@ -802,6 +793,14 @@ impl MailBackend for NotmuchDb {
}))
}
fn delete_messages(
&mut self,
_env_hashes: EnvelopeHashBatch,
_mailbox_hash: MailboxHash,
) -> ResultFuture<()> {
Err(MeliError::new("Unimplemented."))
}
fn search(
&self,
melib_query: crate::search::Query,
@ -813,7 +812,6 @@ impl MailBackend for NotmuchDb {
self.lib.clone(),
false,
)?;
let lib = self.lib.clone();
let mailboxes = self.mailboxes.clone();
Ok(Box::pin(async move {
let mut ret = SmallVec::new();
@ -830,7 +828,7 @@ impl MailBackend for NotmuchDb {
String::new()
};
melib_query.query_to_string(&mut query_s);
let query: Query = Query::new(lib.clone(), &database, &query_s)?;
let query: Query = Query::new(&database, &query_s)?;
let iter = query.search()?;
for message in iter {
ret.push(message.env_hash());
@ -840,8 +838,8 @@ impl MailBackend for NotmuchDb {
}))
}
fn tags(&self) -> Option<Arc<RwLock<BTreeMap<u64, String>>>> {
Some(self.tag_index.clone())
fn collection(&self) -> Collection {
self.collection.clone()
}
fn as_any(&self) -> &dyn Any {
@ -857,7 +855,7 @@ impl MailBackend for NotmuchDb {
struct NotmuchOp {
hash: EnvelopeHash,
index: Arc<RwLock<HashMap<EnvelopeHash, CString>>>,
tag_index: Arc<RwLock<BTreeMap<u64, String>>>,
collection: Collection,
database: Arc<DbConnection>,
bytes: Option<Vec<u8>>,
lib: Arc<libloading::Library>,
@ -890,11 +888,8 @@ pub struct Query<'s> {
}
impl<'s> Query<'s> {
fn new(
lib: Arc<libloading::Library>,
database: &DbConnection,
query_str: &'s str,
) -> Result<Self> {
fn new(database: &DbConnection, query_str: &'s str) -> Result<Self> {
let lib: Arc<libloading::Library> = database.lib.clone();
let query_cstr = std::ffi::CString::new(query_str)?;
let query: *mut notmuch_query_t = unsafe {
call!(lib, notmuch_query_create)(*database.inner.read().unwrap(), query_cstr.as_ptr())

View File

@ -65,6 +65,16 @@ impl<'m> Message<'m> {
}
}
pub fn header(&self, header: &CStr) -> Option<&[u8]> {
let header_val =
unsafe { call!(self.lib, notmuch_message_get_header)(self.message, header.as_ptr()) };
if header_val.is_null() {
None
} else {
Some(unsafe { CStr::from_ptr(header_val).to_bytes() })
}
}
pub fn msg_id(&self) -> &[u8] {
let c_str = self.msg_id_cstr();
c_str.to_bytes()
@ -81,19 +91,11 @@ impl<'m> Message<'m> {
pub fn into_envelope(
self,
index: Arc<RwLock<HashMap<EnvelopeHash, CString>>>,
tag_index: Arc<RwLock<BTreeMap<u64, String>>>,
) -> Result<Envelope> {
let mut contents = Vec::new();
let path = self.get_filename().to_os_string();
let mut f = std::fs::File::open(&path)?;
f.read_to_end(&mut contents)?;
index: &RwLock<HashMap<EnvelopeHash, CString>>,
tag_index: &RwLock<BTreeMap<u64, String>>,
) -> Envelope {
let env_hash = self.env_hash();
let mut env = Envelope::from_bytes(&contents, None).chain_err_summary(|| {
index.write().unwrap().remove(&env_hash);
format!("could not parse path {:?}", path)
})?;
env.set_hash(env_hash);
let mut env = Envelope::new(env_hash);
index
.write()
.unwrap()
@ -109,8 +111,63 @@ impl<'m> Message<'m> {
}
env.labels_mut().push(num);
}
env.set_flags(flags);
Ok(env)
unsafe {
use crate::email::parser::address::rfc2822address_list;
env.set_message_id(self.msg_id())
.set_date(
self.header(CStr::from_bytes_with_nul_unchecked(b"Date\0"))
.unwrap_or_default(),
)
.set_from(
rfc2822address_list(
self.header(CStr::from_bytes_with_nul_unchecked(b"From\0"))
.unwrap_or_default(),
)
.map(|(_, v)| v)
.unwrap_or_default(),
)
.set_to(
rfc2822address_list(
self.header(CStr::from_bytes_with_nul_unchecked(b"To\0"))
.unwrap_or_default(),
)
.map(|(_, v)| v)
.unwrap_or_default(),
)
.set_cc(
rfc2822address_list(
self.header(CStr::from_bytes_with_nul_unchecked(b"Cc\0"))
.unwrap_or_default(),
)
.map(|(_, v)| v)
.unwrap_or_default(),
)
.set_bcc(
rfc2822address_list(
self.header(CStr::from_bytes_with_nul_unchecked(b"Bcc\0"))
.unwrap_or_default(),
)
.map(|(_, v)| v)
.unwrap_or_default()
.to_vec(),
)
.set_subject(
self.header(CStr::from_bytes_with_nul_unchecked(b"Subject\0"))
.unwrap_or_default()
.to_vec(),
)
.set_references(
self.header(CStr::from_bytes_with_nul_unchecked(b"References\0"))
.unwrap_or_default(),
)
.set_in_reply_to(
self.header(CStr::from_bytes_with_nul_unchecked(b"In-Reply-To\0"))
.unwrap_or_default(),
)
.set_datetime(self.date())
.set_flags(flags);
}
env
}
pub fn replies_iter(&self) -> Option<MessageIterator> {
@ -137,6 +194,7 @@ impl<'m> Message<'m> {
ThreadNode {
message: Some(self.env_hash()),
parent: None,
other_mailbox: false,
children: vec![],
date: self.date(),
show_subject: true,

View File

@ -25,7 +25,7 @@ use smallvec::SmallVec;
use std::ops::{Deref, DerefMut};
use std::sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard};
use std::collections::{HashMap, HashSet};
use std::collections::{BTreeMap, HashMap, HashSet};
pub struct EnvelopeRef<'g> {
guard: RwLockReadGuard<'g, HashMap<EnvelopeHash, Envelope>>,
@ -64,8 +64,9 @@ pub struct Collection {
pub envelopes: Arc<RwLock<HashMap<EnvelopeHash, Envelope>>>,
pub message_id_index: Arc<RwLock<HashMap<Vec<u8>, EnvelopeHash>>>,
pub threads: Arc<RwLock<HashMap<MailboxHash, Threads>>>,
sent_mailbox: Arc<RwLock<Option<MailboxHash>>>,
pub sent_mailbox: Arc<RwLock<Option<MailboxHash>>>,
pub mailboxes: Arc<RwLock<HashMap<MailboxHash, HashSet<EnvelopeHash>>>>,
pub tag_index: Arc<RwLock<BTreeMap<u64, String>>>,
}
impl Default for Collection {
@ -88,7 +89,11 @@ impl Drop for Collection {
}
};
let writer = io::BufWriter::new(f);
bincode::serialize_into(writer, &self.threads).unwrap();
let _ = bincode::Options::serialize_into(
bincode::config::DefaultOptions::new(),
writer,
&self.thread,
);
}
}
}
@ -111,6 +116,7 @@ impl Collection {
Collection {
envelopes: Arc::new(RwLock::new(Default::default())),
tag_index: Arc::new(RwLock::new(BTreeMap::default())),
message_id_index,
threads,
mailboxes,

View File

@ -128,6 +128,7 @@ pub enum ToggleFlag {
InternalVal(bool),
False,
True,
Ask,
}
impl From<bool> for ToggleFlag {
@ -157,9 +158,15 @@ impl ToggleFlag {
false
}
}
pub fn is_ask(&self) -> bool {
*self == ToggleFlag::Ask
}
pub fn is_false(&self) -> bool {
ToggleFlag::False == *self || ToggleFlag::InternalVal(false) == *self
}
pub fn is_true(&self) -> bool {
ToggleFlag::True == *self || ToggleFlag::InternalVal(true) == *self
}
@ -174,6 +181,7 @@ impl Serialize for ToggleFlag {
ToggleFlag::Unset | ToggleFlag::InternalVal(_) => serializer.serialize_none(),
ToggleFlag::False => serializer.serialize_bool(false),
ToggleFlag::True => serializer.serialize_bool(true),
ToggleFlag::Ask => serializer.serialize_str("ask"),
}
}
}
@ -183,10 +191,17 @@ impl<'de> Deserialize<'de> for ToggleFlag {
where
D: Deserializer<'de>,
{
let s = <bool>::deserialize(deserializer);
let s = <String>::deserialize(deserializer);
Ok(match s? {
true => ToggleFlag::True,
false => ToggleFlag::False,
s if s.eq_ignore_ascii_case("true") => ToggleFlag::True,
s if s.eq_ignore_ascii_case("false") => ToggleFlag::False,
s if s.eq_ignore_ascii_case("ask") => ToggleFlag::Ask,
s => {
return Err(serde::de::Error::custom(format!(
r#"expected one of "true", "false", "ask", found `{}`"#,
s
)))
}
})
}
}

View File

@ -291,3 +291,7 @@ pub async fn timeout<O>(dur: Option<Duration>, f: impl Future<Output = O>) -> cr
Ok(f.await)
}
}
pub async fn sleep(dur: Duration) {
smol::Timer::after(dur).await;
}

View File

@ -34,67 +34,133 @@
//! assert_eq!(timestamp, 1578509043);
//!
//! // Convert timestamp back to string
//! let s = timestamp_to_string(timestamp, Some("%Y-%m-%d"));
//! let s = timestamp_to_string(timestamp, Some("%Y-%m-%d"), true);
//! assert_eq!(s, "2020-01-08");
//! ```
use crate::error::Result;
use crate::error::{Result, ResultIntoMeliError};
use std::borrow::Cow;
use std::convert::TryInto;
use std::ffi::{CStr, CString};
pub type UnixTimestamp = u64;
use libc::{timeval, timezone};
pub const RFC3339_FMT_WITH_TIME: &str = "%Y-%m-%dT%H:%M:%S\0";
pub const RFC3339_FMT: &str = "%Y-%m-%d\0";
pub const RFC822_FMT_WITH_TIME: &str = "%a, %e %h %Y %H:%M:%S \0";
pub const RFC822_FMT: &str = "%e %h %Y %H:%M:%S \0";
pub const DEFAULT_FMT: &str = "%a, %d %b %Y %R\0";
//"Tue May 21 13:46:22 1991\n"
pub const ASCTIME_FMT: &str = "%a %b %d %H:%M:%S %Y\n\0";
extern "C" {
fn strptime(
s: *const ::std::os::raw::c_char,
format: *const ::std::os::raw::c_char,
tm: *mut ::libc::tm,
) -> *const ::std::os::raw::c_char;
s: *const std::os::raw::c_char,
format: *const std::os::raw::c_char,
tm: *mut libc::tm,
) -> *const std::os::raw::c_char;
fn strftime(
s: *mut ::std::os::raw::c_char,
max: ::libc::size_t,
format: *const ::std::os::raw::c_char,
tm: *const ::libc::tm,
) -> ::libc::size_t;
s: *mut std::os::raw::c_char,
max: libc::size_t,
format: *const std::os::raw::c_char,
tm: *const libc::tm,
) -> libc::size_t;
fn mktime(tm: *const ::libc::tm) -> ::libc::time_t;
fn mktime(tm: *const libc::tm) -> libc::time_t;
fn localtime_r(timep: *const ::libc::time_t, tm: *mut ::libc::tm) -> *mut ::libc::tm;
fn localtime_r(timep: *const libc::time_t, tm: *mut libc::tm) -> *mut libc::tm;
fn gettimeofday(tv: *mut timeval, tz: *mut timezone) -> i32;
fn gettimeofday(tv: *mut libc::timeval, tz: *mut libc::timezone) -> i32;
}
pub fn timestamp_to_string(timestamp: UnixTimestamp, fmt: Option<&str>) -> String {
let mut new_tm: ::libc::tm = unsafe { std::mem::zeroed() };
struct Locale {
new_locale: libc::locale_t,
old_locale: libc::locale_t,
}
impl Drop for Locale {
fn drop(&mut self) {
unsafe {
let _ = libc::uselocale(self.old_locale);
libc::freelocale(self.new_locale);
}
}
}
// How to unit test this? Test machine is not guaranteed to have non-english locales.
impl Locale {
fn new(
mask: std::os::raw::c_int,
locale: *const std::os::raw::c_char,
base: libc::locale_t,
) -> Result<Self> {
let new_locale = unsafe { libc::newlocale(mask, locale, base) };
if new_locale.is_null() {
return Err(nix::Error::last().into());
}
let old_locale = unsafe { libc::uselocale(new_locale) };
if old_locale.is_null() {
unsafe { libc::freelocale(new_locale) };
return Err(nix::Error::last().into());
}
Ok(Locale {
new_locale,
old_locale,
})
}
}
pub fn timestamp_to_string(timestamp: UnixTimestamp, fmt: Option<&str>, posix: bool) -> String {
let mut new_tm: libc::tm = unsafe { std::mem::zeroed() };
unsafe {
let i: i64 = timestamp.try_into().unwrap_or(0);
localtime_r(&i as *const i64, &mut new_tm as *mut ::libc::tm);
localtime_r(&i as *const i64, &mut new_tm as *mut libc::tm);
}
let fmt = fmt
let format: Cow<'_, CStr> = if let Some(cs) = fmt
.map(str::as_bytes)
.map(CStr::from_bytes_with_nul)
.and_then(|res| res.ok())
{
Cow::from(cs)
} else if let Some(cstring) = fmt
.map(str::as_bytes)
.map(CString::new)
.map(|res| res.ok())
.and_then(|opt| opt);
let format: &CStr = if let Some(ref s) = fmt {
&s
.and_then(|res| res.ok())
{
Cow::from(cstring)
} else {
unsafe { CStr::from_bytes_with_nul_unchecked(b"%a, %d %b %Y %T %z\0") }
unsafe { CStr::from_bytes_with_nul_unchecked(DEFAULT_FMT.as_bytes()).into() }
};
let mut vec: [u8; 256] = [0; 256];
let ret = unsafe {
strftime(
vec.as_mut_ptr() as *mut _,
256,
format.as_ptr(),
&new_tm as *const _,
)
let ret = {
let _with_locale: Option<Result<Locale>> = if posix {
Some(
Locale::new(
libc::LC_TIME,
b"C\0".as_ptr() as *const i8,
std::ptr::null_mut(),
)
.chain_err_summary(|| "Could not set locale for datetime conversion")
.chain_err_kind(crate::error::ErrorKind::External),
)
} else {
None
};
unsafe {
strftime(
vec.as_mut_ptr() as *mut _,
256,
format.as_ptr(),
&new_tm as *const _,
)
}
};
String::from_utf8_lossy(&vec[0..ret]).into_owned()
}
fn tm_to_secs(tm: ::libc::tm) -> std::result::Result<i64, ()> {
fn tm_to_secs(tm: libc::tm) -> std::result::Result<i64, ()> {
let mut is_leap = false;
let mut year = tm.tm_year;
let mut month = tm.tm_mon;
@ -207,52 +273,58 @@ where
T: Into<Vec<u8>>,
{
let s = CString::new(s)?;
let mut new_tm: ::libc::tm = unsafe { std::mem::zeroed() };
for fmt in &[
&b"%a, %e %h %Y %H:%M:%S \0"[..],
&b"%e %h %Y %H:%M:%S \0"[..],
] {
unsafe {
let fmt = CStr::from_bytes_with_nul_unchecked(fmt);
let ret = strptime(s.as_ptr(), fmt.as_ptr(), &mut new_tm as *mut _);
if ret.is_null() {
continue;
}
let rest = CStr::from_ptr(ret);
let tm_gmtoff = if rest.to_bytes().len() > 4
&& rest.to_bytes().is_ascii()
&& rest.to_bytes()[1..5].iter().all(u8::is_ascii_digit)
{
let offset = std::str::from_utf8_unchecked(&rest.to_bytes()[0..5]);
if let (Ok(mut hr_offset), Ok(mut min_offset)) =
(offset[1..3].parse::<i64>(), offset[3..5].parse::<i64>())
{
if rest.to_bytes()[0] == b'-' {
hr_offset = -hr_offset;
min_offset = -min_offset;
}
hr_offset * 60 * 60 + min_offset * 60
} else {
0
}
} else {
let rest = if rest.to_bytes().starts_with(b"(") && rest.to_bytes().ends_with(b")") {
&rest.to_bytes()[1..rest.to_bytes().len() - 1]
} else {
rest.to_bytes()
};
let mut new_tm: libc::tm = unsafe { std::mem::zeroed() };
for fmt in &[RFC822_FMT_WITH_TIME, RFC822_FMT] {
let fmt = unsafe { CStr::from_bytes_with_nul_unchecked(fmt.as_bytes()) };
let ret = {
let _with_locale = Locale::new(
libc::LC_TIME,
b"C\0".as_ptr() as *const i8,
std::ptr::null_mut(),
)
.chain_err_summary(|| "Could not set locale for datetime conversion")
.chain_err_kind(crate::error::ErrorKind::External)?;
unsafe { strptime(s.as_ptr(), fmt.as_ptr(), &mut new_tm as *mut _) }
};
if let Ok(idx) = TIMEZONE_ABBR.binary_search_by(|probe| probe.0.cmp(rest)) {
let (hr_offset, min_offset) = TIMEZONE_ABBR[idx].1;
(hr_offset as i64) * 60 * 60 + (min_offset as i64) * 60
} else {
0
}
};
return Ok(tm_to_secs(new_tm)
.map(|res| (res - tm_gmtoff) as u64)
.unwrap_or(0));
if ret.is_null() {
continue;
}
let rest = unsafe { CStr::from_ptr(ret) };
let tm_gmtoff = if rest.to_bytes().len() > 4
&& rest.to_bytes().is_ascii()
&& rest.to_bytes()[1..5].iter().all(u8::is_ascii_digit)
{
// safe since rest.to_bytes().is_ascii()
let offset = unsafe { std::str::from_utf8_unchecked(&rest.to_bytes()[0..5]) };
if let (Ok(mut hr_offset), Ok(mut min_offset)) =
(offset[1..3].parse::<i64>(), offset[3..5].parse::<i64>())
{
if rest.to_bytes()[0] == b'-' {
hr_offset = -hr_offset;
min_offset = -min_offset;
}
hr_offset * 60 * 60 + min_offset * 60
} else {
0
}
} else {
let rest = if rest.to_bytes().starts_with(b"(") && rest.to_bytes().ends_with(b")") {
&rest.to_bytes()[1..rest.to_bytes().len() - 1]
} else {
rest.to_bytes()
};
if let Ok(idx) = TIMEZONE_ABBR.binary_search_by(|probe| probe.0.cmp(rest)) {
let (hr_offset, min_offset) = TIMEZONE_ABBR[idx].1;
(hr_offset as i64) * 60 * 60 + (min_offset as i64) * 60
} else {
0
}
};
return Ok(tm_to_secs(new_tm)
.map(|res| (res - tm_gmtoff) as u64)
.unwrap_or(0));
}
Ok(0)
}
@ -262,50 +334,58 @@ where
T: Into<Vec<u8>>,
{
let s = CString::new(s)?;
let mut new_tm: ::libc::tm = unsafe { std::mem::zeroed() };
for fmt in &[&b"%Y-%m-%dT%H:%M:%S\0"[..], &b"%Y-%m-%d\0"[..]] {
unsafe {
let fmt = CStr::from_bytes_with_nul_unchecked(fmt);
let ret = strptime(s.as_ptr(), fmt.as_ptr(), &mut new_tm as *mut _);
if ret.is_null() {
continue;
}
let rest = CStr::from_ptr(ret);
let tm_gmtoff = if rest.to_bytes().len() > 4
&& rest.to_bytes().is_ascii()
&& rest.to_bytes()[1..3].iter().all(u8::is_ascii_digit)
&& rest.to_bytes()[4..6].iter().all(u8::is_ascii_digit)
{
let offset = std::str::from_utf8_unchecked(&rest.to_bytes()[0..6]);
if let (Ok(mut hr_offset), Ok(mut min_offset)) =
(offset[1..3].parse::<i64>(), offset[4..6].parse::<i64>())
{
if rest.to_bytes()[0] == b'-' {
hr_offset = -hr_offset;
min_offset = -min_offset;
}
hr_offset * 60 * 60 + min_offset * 60
} else {
0
}
} else {
let rest = if rest.to_bytes().starts_with(b"(") && rest.to_bytes().ends_with(b")") {
&rest.to_bytes()[1..rest.to_bytes().len() - 1]
} else {
rest.to_bytes()
};
if let Ok(idx) = TIMEZONE_ABBR.binary_search_by(|probe| probe.0.cmp(rest)) {
let (hr_offset, min_offset) = debug!(TIMEZONE_ABBR[idx]).1;
(hr_offset as i64) * 60 * 60 + (min_offset as i64) * 60
} else {
0
}
};
return Ok(tm_to_secs(new_tm)
.map(|res| (res - tm_gmtoff) as u64)
.unwrap_or(0));
let mut new_tm: libc::tm = unsafe { std::mem::zeroed() };
for fmt in &[RFC3339_FMT_WITH_TIME, RFC3339_FMT] {
let fmt = unsafe { CStr::from_bytes_with_nul_unchecked(fmt.as_bytes()) };
let ret = {
let _with_locale = Locale::new(
libc::LC_TIME,
b"C\0".as_ptr() as *const i8,
std::ptr::null_mut(),
)
.chain_err_summary(|| "Could not set locale for datetime conversion")
.chain_err_kind(crate::error::ErrorKind::External)?;
unsafe { strptime(s.as_ptr(), fmt.as_ptr(), &mut new_tm as *mut _) }
};
if ret.is_null() {
continue;
}
let rest = unsafe { CStr::from_ptr(ret) };
let tm_gmtoff = if rest.to_bytes().len() > 4
&& rest.to_bytes().is_ascii()
&& rest.to_bytes()[1..3].iter().all(u8::is_ascii_digit)
&& rest.to_bytes()[4..6].iter().all(u8::is_ascii_digit)
{
// safe since rest.to_bytes().is_ascii()
let offset = unsafe { std::str::from_utf8_unchecked(&rest.to_bytes()[0..6]) };
if let (Ok(mut hr_offset), Ok(mut min_offset)) =
(offset[1..3].parse::<i64>(), offset[4..6].parse::<i64>())
{
if rest.to_bytes()[0] == b'-' {
hr_offset = -hr_offset;
min_offset = -min_offset;
}
hr_offset * 60 * 60 + min_offset * 60
} else {
0
}
} else {
let rest = if rest.to_bytes().starts_with(b"(") && rest.to_bytes().ends_with(b")") {
&rest.to_bytes()[1..rest.to_bytes().len() - 1]
} else {
rest.to_bytes()
};
if let Ok(idx) = TIMEZONE_ABBR.binary_search_by(|probe| probe.0.cmp(rest)) {
let (hr_offset, min_offset) = TIMEZONE_ABBR[idx].1;
(hr_offset as i64) * 60 * 60 + (min_offset as i64) * 60
} else {
0
}
};
return Ok(tm_to_secs(new_tm)
.map(|res| (res - tm_gmtoff) as u64)
.unwrap_or(0));
}
Ok(0)
}
@ -315,8 +395,12 @@ pub fn timestamp_from_string<T>(s: T, fmt: &str) -> Result<Option<UnixTimestamp>
where
T: Into<Vec<u8>>,
{
let mut new_tm: ::libc::tm = unsafe { std::mem::zeroed() };
let fmt = CString::new(fmt)?;
let mut new_tm: libc::tm = unsafe { std::mem::zeroed() };
let fmt: Cow<'_, CStr> = if let Ok(cs) = CStr::from_bytes_with_nul(fmt.as_bytes()) {
Cow::from(cs)
} else {
Cow::from(CString::new(fmt.as_bytes())?)
};
unsafe {
let ret = strptime(
CString::new(s)?.as_ptr(),
@ -332,8 +416,8 @@ where
pub fn now() -> UnixTimestamp {
use std::mem::MaybeUninit;
let mut tv = MaybeUninit::<::libc::timeval>::uninit();
let mut tz = MaybeUninit::<::libc::timezone>::uninit();
let mut tv = MaybeUninit::<libc::timeval>::uninit();
let mut tz = MaybeUninit::<libc::timezone>::uninit();
unsafe {
let ret = gettimeofday(tv.as_mut_ptr(), tz.as_mut_ptr());
if ret == -1 {
@ -344,12 +428,15 @@ pub fn now() -> UnixTimestamp {
}
#[test]
fn test_timestamp() {
timestamp_to_string(0, None);
fn test_datetime_timestamp() {
timestamp_to_string(0, None, false);
}
#[test]
fn test_rfcs() {
fn test_datetime_rfcs() {
if unsafe { libc::setlocale(libc::LC_ALL, b"\0".as_ptr() as _) }.is_null() {
println!("Unable to set locale.");
}
/* Some tests were lazily stolen from https://rachelbythebay.com/w/2013/06/11/time/ */
assert_eq!(
@ -360,7 +447,7 @@ fn test_rfcs() {
/*
macro_rules! mkt {
($year:literal, $month:literal, $day:literal, $hour:literal, $minute:literal, $second:literal) => {
::libc::tm {
libc::tm {
tm_sec: $second,
tm_min: $minute,
tm_hour: $hour,

View File

@ -20,8 +20,75 @@
*/
/*!
* Email parsing, handling, sending etc.
* Email parsing and composing.
*
* # Parsing bytes into an `Envelope`
*
* An [`Envelope`](Envelope) represents the information you can get from an email's headers and body
* structure. Addresses in `To`, `From` fields etc are parsed into [`Address`](email::address::Address) types.
*
* ```
* use melib::{Attachment, Envelope};
*
* let raw_mail = r#"From: "some name" <some@example.com>
* To: "me" <myself@example.com>
* Cc:
* Subject: =?utf-8?Q?gratuitously_encoded_subject?=
* Message-ID: <h2g7f.z0gy2pgaen5m@example.com>
* MIME-Version: 1.0
* Content-Type: multipart/mixed; charset="utf-8";
* boundary="bzz_bzz__bzz__"
*
* This is a MIME formatted message with attachments. Use a MIME-compliant client to view it properly.
* --bzz_bzz__bzz__
*
* hello world.
* --bzz_bzz__bzz__
* Content-Type: image/gif; name="test_image.gif"; charset="utf-8"
* Content-Disposition: attachment
* Content-Transfer-Encoding: base64
*
* R0lGODdhKAAXAOfZAAABzAADzQAEzgQFtBEAxAAGxBcAxwALvRcFwAAPwBcLugATuQEUuxoNuxYQ
* sxwOvAYVvBsStSAVtx8YsRUcuhwhth4iuCQsyDAwuDc1vTc3uDg4uT85rkc9ukJBvENCvURGukdF
* wUVKt0hLuUxPvVZSvFlYu1hbt2BZuFxdul5joGhqlnNuf3FvlnBvwXJyt3Jxw3N0oXx1gH12gV99
* z317f3N7spFxwHp5wH99gYB+goF/g25+26tziIOBhWqD3oiBjICAuudkjIN+zHeC2n6Bzc1vh4eF
* iYaBw8F0kImHi4KFxYyHmIWIvI2Lj4uIvYaJyY+IuJGMi5iJl4qKxZSMmIuLxpONnpGPk42NvI2M
* 1LKGl46OvZePm5ORlZiQnJqSnpaUmLyJnJuTn5iVmZyUoJGVyZ2VoZSVw5iXoZmWrO18rJiUyp6W
* opuYnKaVnZ+Xo5yZncaMoaCYpJiaqo+Z2Z2annuf5qGZpa2WoJybpZmayZ2Z0KCZypydrZ6dp6Cd
* oZ6a0aGay5ucy5+eqKGeouWMgp+b0qKbzKCfqdqPnp2ezaGgqqOgpKafqrScpp+gz6ajqKujr62j
* qayksKmmq62lsaiosqqorOyWnaqqtKeqzLGptaurta2rr7Kqtq+ssLOrt6+uuLGusuqhfbWtubCv
* ubKvs7GwurOwtPSazbevu+ali7SxtbiwvOykjLOyvLWytuCmqOankrSzvbazuLmyvrW0vre0uba1
* wLi1ury0wLm2u721wbe3wbq3vMC2vLi4wr+3w7m5w8C4xLi6yry6vsG5xbu7xcC6zMK6xry8xry+
* u8O7x729x8C9wb++yMG+wsO+vMK/w8a+y8e/zMnBzcXH18nL2///////////////////////////
* ////////////////////////////////////////////////////////////////////////////
* /////////////////////////////////////////////////////ywAAAAAKAAXAAAI/gBP4Cjh
* IYMLEh0w4EgBgsMLEyFGFBEB5cOFABgzatS4AVssZAOsLOHCxooVMzCyoNmzaBOkJlS0VEDyZMjG
* mxk3XOMF60CDBgsoPABK9KcDCRImPCiQYAECAgQCRMU4VSrGCjFarBgUSJCgQ10FBTrkNRCfPnz4
* dA3UNa1btnDZqgU7Ntqzu3ej2X2mFy9eaHuhNRtMGJrhwYYN930G2K7eaNIY34U2mfJkwpgzI9Yr
* GBqwR2KSvAlMOXHnw5pTNzPdLNoWIWtU9XjGjDEYS8LAlFm1SrVvzIKj5TH0KpORSZOryPgCZgqL
* Ob+jG0YVRBErUrOiiGJ8KxgtYsh27xWL/tswnTtEbsiRVYdJNMHk4yOGhswGjR88UKjQ9Ey+/8TL
* XKKGGn7Akph/8XX2WDTTcAYfguVt9hhrEPqmzIOJ3VUheb48WJiHG6amC4i+WVJKKCimqGIoYxyj
* WWK8kKjaJ9bA18sxvXjYhourmbbMMrjI+OIn1QymDCVXANGFK4S1gQw0PxozzC+33FLLKUJq9gk1
* gyWDhyNwrMLkYGUEM4wvuLRiCiieXIJJJVlmJskcZ9TZRht1lnFGGmTMkMoonVQSSSOFAGJHHI0w
* ouiijDaaCCGQRgrpH3q4QYYXWDihxBE+7KCDDjnUIEVAADs=
* --bzz_bzz__bzz__--"#;
*
* let envelope = Envelope::from_bytes(raw_mail.as_bytes(), None).expect("Could not parse mail");
* assert_eq!(envelope.subject().as_ref(), "gratuitously encoded subject");
* assert_eq!(envelope.message_id_display().as_ref(), "<h2g7f.z0gy2pgaen5m@example.com>");
*
* let body = envelope.body_bytes(raw_mail.as_bytes());
* assert_eq!(body.content_type().to_string().as_str(), "multipart/mixed");
*
* let body_text = body.text();
* assert_eq!(body_text.as_str(), "hello world.");
*
* let subattachments: Vec<Attachment> = body.attachments();
* assert_eq!(subattachments.len(), 3);
* assert_eq!(subattachments[2].content_type().name().unwrap(), "test_image.gif");
* ```
*/
pub mod address;
pub mod attachment_types;
pub mod attachments;
@ -30,7 +97,7 @@ pub mod headers;
pub mod list_management;
pub mod mailto;
pub mod parser;
pub mod signatures;
pub mod pgp;
pub use address::{Address, MessageID, References, StrBuild, StrBuilder};
pub use attachments::{Attachment, AttachmentBuilder};
@ -76,6 +143,23 @@ impl PartialEq<&str> for Flag {
}
}
macro_rules! flag_impl {
(fn $name:ident, $val:expr) => {
pub const fn $name(&self) -> bool {
self.contains($val)
}
};
}
impl Flag {
flag_impl!(fn is_seen, Flag::SEEN);
flag_impl!(fn is_draft, Flag::DRAFT);
flag_impl!(fn is_trashed, Flag::TRASHED);
flag_impl!(fn is_passed, Flag::PASSED);
flag_impl!(fn is_replied, Flag::REPLIED);
flag_impl!(fn is_flagged, Flag::FLAGGED);
}
///`Mail` holds both the envelope info of an email in its `envelope` field and the raw bytes that
///describe the email in `bytes`. Its body as an `melib::email::Attachment` can be parsed on demand
///with the `melib::email::Mail::body` method.
@ -152,6 +236,7 @@ impl core::fmt::Debug for Envelope {
.field("Message-ID", &self.message_id_display())
.field("In-Reply-To", &self.in_reply_to_display())
.field("References", &self.references)
.field("Flags", &self.flags)
.field("Hash", &self.hash)
.finish()
}
@ -185,8 +270,9 @@ impl Envelope {
}
}
pub fn set_hash(&mut self, new_hash: EnvelopeHash) {
pub fn set_hash(&mut self, new_hash: EnvelopeHash) -> &mut Self {
self.hash = new_hash;
self
}
pub fn from_bytes(bytes: &[u8], flags: Option<Flag>) -> Result<Envelope> {
@ -252,14 +338,6 @@ impl Envelope {
} else if name == "message-id" {
self.set_message_id(value);
} else if name == "references" {
{
let parse_result = parser::address::msg_id_list(value);
if let Ok((_, value)) = parse_result {
for v in value {
self.push_references(v);
}
}
}
self.set_references(value);
} else if name == "in-reply-to" {
self.set_in_reply_to(value);
@ -506,41 +584,51 @@ impl Envelope {
String::from_utf8_lossy(self.message_id.raw())
}
pub fn set_date(&mut self, new_val: &[u8]) {
pub fn set_date(&mut self, new_val: &[u8]) -> &mut Self {
let new_val = new_val.trim();
self.date = String::from_utf8_lossy(new_val).into_owned();
self
}
pub fn set_bcc(&mut self, new_val: Vec<Address>) {
pub fn set_bcc(&mut self, new_val: Vec<Address>) -> &mut Self {
self.bcc = new_val;
self
}
pub fn set_cc(&mut self, new_val: SmallVec<[Address; 1]>) {
pub fn set_cc(&mut self, new_val: SmallVec<[Address; 1]>) -> &mut Self {
self.cc = new_val;
self
}
pub fn set_from(&mut self, new_val: SmallVec<[Address; 1]>) {
pub fn set_from(&mut self, new_val: SmallVec<[Address; 1]>) -> &mut Self {
self.from = new_val;
self
}
pub fn set_to(&mut self, new_val: SmallVec<[Address; 1]>) {
pub fn set_to(&mut self, new_val: SmallVec<[Address; 1]>) -> &mut Self {
self.to = new_val;
self
}
pub fn set_in_reply_to(&mut self, new_val: &[u8]) {
pub fn set_in_reply_to(&mut self, new_val: &[u8]) -> &mut Self {
// FIXME msg_id_list
let new_val = new_val.trim();
let val = match parser::address::msg_id(new_val) {
Ok(v) => v.1,
Err(_) => {
self.in_reply_to = Some(MessageID::new(new_val, new_val));
return;
}
};
self.in_reply_to = Some(val);
if !new_val.is_empty() {
let val = match parser::address::msg_id(new_val) {
Ok(v) => v.1,
Err(_) => {
self.in_reply_to = Some(MessageID::new(new_val, new_val));
return self;
}
};
self.in_reply_to = Some(val);
} else {
self.in_reply_to = None;
}
self
}
pub fn set_subject(&mut self, new_val: Vec<u8>) {
pub fn set_subject(&mut self, new_val: Vec<u8>) -> &mut Self {
let mut new_val = String::from_utf8(new_val)
.unwrap_or_else(|err| String::from_utf8_lossy(&err.into_bytes()).into());
while new_val
@ -553,9 +641,10 @@ impl Envelope {
}
self.subject = Some(new_val);
self
}
pub fn set_message_id(&mut self, new_val: &[u8]) {
pub fn set_message_id(&mut self, new_val: &[u8]) -> &mut Self {
let new_val = new_val.trim();
match parser::address::msg_id(new_val) {
Ok((_, val)) => {
@ -565,6 +654,7 @@ impl Envelope {
self.message_id = MessageID::new(new_val, new_val);
}
}
self
}
pub fn push_references(&mut self, new_ref: MessageID) {
@ -593,19 +683,31 @@ impl Envelope {
}
}
pub fn set_references(&mut self, new_val: &[u8]) {
pub fn set_references(&mut self, new_val: &[u8]) -> &mut Self {
let new_val = new_val.trim();
match self.references {
Some(ref mut s) => {
s.raw = new_val.into();
if !new_val.is_empty() {
self.references = None;
{
let parse_result = parser::address::msg_id_list(new_val);
if let Ok((_, value)) = parse_result {
for v in value {
self.push_references(v);
}
}
}
None => {
self.references = Some(References {
raw: new_val.into(),
refs: Vec::new(),
});
match self.references {
Some(ref mut s) => {
s.raw = new_val.into();
}
None => {
self.references = Some(References {
raw: new_val.into(),
refs: Vec::new(),
});
}
}
}
self
}
pub fn references(&self) -> SmallVec<[&MessageID; 8]> {
@ -630,40 +732,47 @@ impl Envelope {
self.thread
}
pub fn set_thread(&mut self, new_val: ThreadNodeHash) {
pub fn set_thread(&mut self, new_val: ThreadNodeHash) -> &mut Self {
self.thread = new_val;
self
}
pub fn set_datetime(&mut self, new_val: UnixTimestamp) {
pub fn set_datetime(&mut self, new_val: UnixTimestamp) -> &mut Self {
self.timestamp = new_val;
self
}
pub fn set_flag(&mut self, f: Flag, value: bool) {
pub fn set_flag(&mut self, f: Flag, value: bool) -> &mut Self {
self.flags.set(f, value);
self
}
pub fn set_flags(&mut self, f: Flag) {
pub fn set_flags(&mut self, f: Flag) -> &mut Self {
self.flags = f;
self
}
pub fn flags(&self) -> Flag {
self.flags
}
pub fn set_seen(&mut self) {
self.set_flag(Flag::SEEN, true)
pub fn set_seen(&mut self) -> &mut Self {
self.set_flag(Flag::SEEN, true);
self
}
pub fn set_unseen(&mut self) {
self.set_flag(Flag::SEEN, false)
pub fn set_unseen(&mut self) -> &mut Self {
self.set_flag(Flag::SEEN, false);
self
}
pub fn is_seen(&self) -> bool {
self.flags.contains(Flag::SEEN)
}
pub fn set_has_attachments(&mut self, new_val: bool) {
pub fn set_has_attachments(&mut self, new_val: bool) -> &mut Self {
self.has_attachments = new_val;
self
}
pub fn has_attachments(&self) -> bool {

View File

@ -31,8 +31,17 @@ pub enum Charset {
UTF16,
ISO8859_1,
ISO8859_2,
ISO8859_3,
ISO8859_4,
ISO8859_5,
ISO8859_6,
ISO8859_7,
ISO8859_8,
ISO8859_10,
ISO8859_13,
ISO8859_14,
ISO8859_15,
ISO8859_16,
Windows1250,
Windows1251,
Windows1252,
@ -41,6 +50,9 @@ pub enum Charset {
GB2312,
BIG5,
ISO2022JP,
EUCJP,
KOI8R,
KOI8U,
}
impl Default for Charset {
@ -67,14 +79,49 @@ impl<'a> From<&'a [u8]> for Charset {
b if b.eq_ignore_ascii_case(b"iso-8859-2") || b.eq_ignore_ascii_case(b"iso8859-2") => {
Charset::ISO8859_2
}
b if b.eq_ignore_ascii_case(b"iso-8859-3") || b.eq_ignore_ascii_case(b"iso8859-3") => {
Charset::ISO8859_3
}
b if b.eq_ignore_ascii_case(b"iso-8859-4") || b.eq_ignore_ascii_case(b"iso8859-4") => {
Charset::ISO8859_4
}
b if b.eq_ignore_ascii_case(b"iso-8859-5") || b.eq_ignore_ascii_case(b"iso8859-5") => {
Charset::ISO8859_5
}
b if b.eq_ignore_ascii_case(b"iso-8859-6") || b.eq_ignore_ascii_case(b"iso8859-6") => {
Charset::ISO8859_6
}
b if b.eq_ignore_ascii_case(b"iso-8859-7") || b.eq_ignore_ascii_case(b"iso8859-7") => {
Charset::ISO8859_7
}
b if b.eq_ignore_ascii_case(b"iso-8859-8") || b.eq_ignore_ascii_case(b"iso8859-8") => {
Charset::ISO8859_8
}
b if b.eq_ignore_ascii_case(b"iso-8859-10")
|| b.eq_ignore_ascii_case(b"iso8859-10") =>
{
Charset::ISO8859_10
}
b if b.eq_ignore_ascii_case(b"iso-8859-13")
|| b.eq_ignore_ascii_case(b"iso8859-13") =>
{
Charset::ISO8859_13
}
b if b.eq_ignore_ascii_case(b"iso-8859-14")
|| b.eq_ignore_ascii_case(b"iso8859-14") =>
{
Charset::ISO8859_14
}
b if b.eq_ignore_ascii_case(b"iso-8859-15")
|| b.eq_ignore_ascii_case(b"iso8859-15") =>
{
Charset::ISO8859_15
}
b if b.eq_ignore_ascii_case(b"iso-8859-16")
|| b.eq_ignore_ascii_case(b"iso8859-16") =>
{
Charset::ISO8859_16
}
b if b.eq_ignore_ascii_case(b"windows-1250")
|| b.eq_ignore_ascii_case(b"windows1250") =>
{
@ -101,6 +148,9 @@ impl<'a> From<&'a [u8]> for Charset {
}
b if b.eq_ignore_ascii_case(b"big5") => Charset::BIG5,
b if b.eq_ignore_ascii_case(b"iso-2022-jp") => Charset::ISO2022JP,
b if b.eq_ignore_ascii_case(b"euc-jp") => Charset::EUCJP,
b if b.eq_ignore_ascii_case(b"koi8-r") => Charset::KOI8R,
b if b.eq_ignore_ascii_case(b"koi8-u") => Charset::KOI8U,
_ => {
debug!("unknown tag is {:?}", str::from_utf8(b));
Charset::Ascii
@ -117,16 +167,28 @@ impl Display for Charset {
Charset::UTF16 => write!(f, "utf-16"),
Charset::ISO8859_1 => write!(f, "iso-8859-1"),
Charset::ISO8859_2 => write!(f, "iso-8859-2"),
Charset::ISO8859_3 => write!(f, "iso-8859-3"),
Charset::ISO8859_4 => write!(f, "iso-8859-4"),
Charset::ISO8859_5 => write!(f, "iso-8859-5"),
Charset::ISO8859_6 => write!(f, "iso-8859-6"),
Charset::ISO8859_7 => write!(f, "iso-8859-7"),
Charset::ISO8859_8 => write!(f, "iso-8859-8"),
Charset::ISO8859_10 => write!(f, "iso-8859-10"),
Charset::ISO8859_13 => write!(f, "iso-8859-13"),
Charset::ISO8859_14 => write!(f, "iso-8859-14"),
Charset::ISO8859_15 => write!(f, "iso-8859-15"),
Charset::ISO8859_16 => write!(f, "iso-8859-16"),
Charset::Windows1250 => write!(f, "windows-1250"),
Charset::Windows1251 => write!(f, "windows-1251"),
Charset::Windows1252 => write!(f, "windows-1252"),
Charset::Windows1253 => write!(f, "windows-1253"),
Charset::GBK => write!(f, "GBK"),
Charset::GBK => write!(f, "gbk"),
Charset::GB2312 => write!(f, "gb2312"),
Charset::BIG5 => write!(f, "BIG5"),
Charset::ISO2022JP => write!(f, "ISO-2022-JP"),
Charset::BIG5 => write!(f, "big5"),
Charset::ISO2022JP => write!(f, "iso-2022-jp"),
Charset::EUCJP => write!(f, "euc-jp"),
Charset::KOI8R => write!(f, "koi8-r"),
Charset::KOI8U => write!(f, "koi8-u"),
}
}
}
@ -135,6 +197,7 @@ impl Display for Charset {
pub enum MultipartType {
Alternative,
Digest,
Encrypted,
Mixed,
Related,
Signed,
@ -148,13 +211,18 @@ impl Default for MultipartType {
impl Display for MultipartType {
fn fmt(&self, f: &mut Formatter) -> FmtResult {
match self {
MultipartType::Alternative => write!(f, "multipart/alternative"),
MultipartType::Digest => write!(f, "multipart/digest"),
MultipartType::Mixed => write!(f, "multipart/mixed"),
MultipartType::Related => write!(f, "multipart/related"),
MultipartType::Signed => write!(f, "multipart/signed"),
}
write!(
f,
"{}",
match self {
MultipartType::Alternative => "multipart/alternative",
MultipartType::Digest => "multipart/digest",
MultipartType::Encrypted => "multipart/encrypted",
MultipartType::Mixed => "multipart/mixed",
MultipartType::Related => "multipart/related",
MultipartType::Signed => "multipart/signed",
}
)
}
}
@ -166,6 +234,8 @@ impl From<&[u8]> for MultipartType {
MultipartType::Alternative
} else if val.eq_ignore_ascii_case(b"digest") {
MultipartType::Digest
} else if val.eq_ignore_ascii_case(b"encrypted") {
MultipartType::Encrypted
} else if val.eq_ignore_ascii_case(b"signed") {
MultipartType::Signed
} else if val.eq_ignore_ascii_case(b"related") {
@ -190,6 +260,7 @@ pub enum ContentType {
},
MessageRfc822,
PGPSignature,
CMSSignature,
Other {
tag: Vec<u8>,
name: Option<String>,
@ -209,6 +280,75 @@ impl Default for ContentType {
}
}
impl PartialEq<&str> for ContentType {
fn eq(&self, other: &&str) -> bool {
match (self, *other) {
(
ContentType::Text {
kind: Text::Plain, ..
},
"text/plain",
) => true,
(
ContentType::Text {
kind: Text::Html, ..
},
"text/html",
) => true,
(
ContentType::Multipart {
kind: MultipartType::Alternative,
..
},
"multipart/alternative",
) => true,
(
ContentType::Multipart {
kind: MultipartType::Digest,
..
},
"multipart/digest",
) => true,
(
ContentType::Multipart {
kind: MultipartType::Encrypted,
..
},
"multipart/encrypted",
) => true,
(
ContentType::Multipart {
kind: MultipartType::Mixed,
..
},
"multipart/mixed",
) => true,
(
ContentType::Multipart {
kind: MultipartType::Related,
..
},
"multipart/related",
) => true,
(
ContentType::Multipart {
kind: MultipartType::Signed,
..
},
"multipart/signed",
) => true,
(ContentType::PGPSignature, "application/pgp-signature") => true,
(ContentType::CMSSignature, "application/pkcs7-signature") => true,
(ContentType::MessageRfc822, "message/rfc822") => true,
(ContentType::Other { tag, .. }, _) => {
other.eq_ignore_ascii_case(&String::from_utf8_lossy(&tag))
}
(ContentType::OctetStream { .. }, "application/octet-stream") => true,
_ => false,
}
}
}
impl Display for ContentType {
fn fmt(&self, f: &mut Formatter) -> FmtResult {
match self {
@ -216,6 +356,7 @@ impl Display for ContentType {
ContentType::Multipart { kind: k, .. } => k.fmt(f),
ContentType::Other { ref tag, .. } => write!(f, "{}", String::from_utf8_lossy(tag)),
ContentType::PGPSignature => write!(f, "application/pgp-signature"),
ContentType::CMSSignature => write!(f, "application/pkcs7-signature"),
ContentType::MessageRfc822 => write!(f, "message/rfc822"),
ContentType::OctetStream { .. } => write!(f, "application/octet-stream"),
}

View File

@ -19,6 +19,7 @@
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
/*! Encoding/decoding of attachments */
use crate::email::{
address::StrBuilder,
parser::{self, BytesExt},
@ -201,6 +202,10 @@ impl AttachmentBuilder {
&& cst.eq_ignore_ascii_case(b"pgp-signature")
{
self.content_type = ContentType::PGPSignature;
} else if ct.eq_ignore_ascii_case(b"application")
&& cst.eq_ignore_ascii_case(b"pkcs7-signature")
{
self.content_type = ContentType::CMSSignature;
} else {
let mut name: Option<String> = None;
for (n, v) in params {
@ -350,16 +355,14 @@ pub struct Attachment {
impl fmt::Debug for Attachment {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "Attachment {{\n content_type: {:?},\n content_transfer_encoding: {:?},\n raw: Vec of {} bytes\n, body:\n{}\n}}",
self.content_type,
self.content_transfer_encoding,
self.raw.len(),
{
let mut text = Vec::with_capacity(4096);
self.get_text_recursive(&mut text);
std::str::from_utf8(&text).map(std::string::ToString::to_string).unwrap_or_else(|e| format!("Unicode error {}", e))
}
)
let mut text = Vec::with_capacity(4096);
self.get_text_recursive(&mut text);
f.debug_struct("Attachment")
.field("content_type", &self.content_type)
.field("content_transfer_encoding", &self.content_transfer_encoding)
.field("raw bytes length", &self.raw.len())
.field("body", &String::from_utf8_lossy(&text))
.finish()
}
}
@ -370,44 +373,64 @@ impl fmt::Display for Attachment {
match Mail::new(self.body.display_bytes(&self.raw).to_vec(), None) {
Ok(wrapper) => write!(
f,
"message/rfc822: {} - {} - {}",
"{} - {} - {} [message/rfc822] {}",
wrapper.date(),
wrapper.field_from_to_string(),
wrapper.subject()
wrapper.subject(),
crate::Bytes(self.raw.len()),
),
Err(err) => write!(
f,
"could not parse: {} [message/rfc822] {}",
err,
crate::Bytes(self.raw.len()),
),
Err(e) => write!(f, "{}", e),
}
}
ContentType::PGPSignature => write!(f, "pgp signature {}", self.mime_type()),
ContentType::OctetStream { ref name } => {
write!(f, "{}", name.clone().unwrap_or_else(|| self.mime_type()))
ContentType::PGPSignature => write!(f, "pgp signature [{}]", self.mime_type()),
ContentType::CMSSignature => write!(f, "S/MIME signature [{}]", self.mime_type()),
ContentType::OctetStream { .. } | ContentType::Other { .. } => {
if let Some(name) = self.filename() {
write!(
f,
"\"{}\", [{}] {}",
name,
self.mime_type(),
crate::Bytes(self.raw.len())
)
} else {
write!(
f,
"Data attachment [{}] {}",
self.mime_type(),
crate::Bytes(self.raw.len())
)
}
}
ContentType::Other {
name: Some(ref name),
..
} => write!(f, "\"{}\", [{}]", name, self.mime_type()),
ContentType::Other { .. } => write!(f, "Data attachment of type {}", self.mime_type()),
ContentType::Text { ref parameters, .. }
if parameters
.iter()
.any(|(name, _)| name.eq_ignore_ascii_case(b"name")) =>
{
let name = String::from_utf8_lossy(
parameters
.iter()
.find(|(name, _)| name.eq_ignore_ascii_case(b"name"))
.map(|(_, value)| value)
.unwrap(),
);
write!(f, "\"{}\", [{}]", name, self.mime_type())
ContentType::Text { .. } => {
if let Some(name) = self.filename() {
write!(
f,
"\"{}\", [{}] {}",
name,
self.mime_type(),
crate::Bytes(self.raw.len())
)
} else {
write!(
f,
"Text attachment [{}] {}",
self.mime_type(),
crate::Bytes(self.raw.len())
)
}
}
ContentType::Text { .. } => write!(f, "Text attachment of type {}", self.mime_type()),
ContentType::Multipart {
parts: ref sub_att_vec,
..
} => write!(
f,
"{} attachment with {} subs",
"{} attachment with {} parts",
self.mime_type(),
sub_att_vec.len()
),
@ -530,7 +553,7 @@ impl Attachment {
fn get_text_recursive(&self, text: &mut Vec<u8>) {
match self.content_type {
ContentType::Text { .. } | ContentType::PGPSignature => {
ContentType::Text { .. } | ContentType::PGPSignature | ContentType::CMSSignature => {
text.extend(decode(self, None));
}
ContentType::Multipart {
@ -626,6 +649,16 @@ impl Attachment {
}
}
pub fn is_encrypted(&self) -> bool {
match self.content_type {
ContentType::Multipart {
kind: MultipartType::Encrypted,
..
} => true,
_ => false,
}
}
pub fn is_signed(&self) -> bool {
match self.content_type {
ContentType::Multipart {
@ -640,7 +673,7 @@ impl Attachment {
let mut ret = String::with_capacity(2 * self.raw.len());
fn into_raw_helper(a: &Attachment, ret: &mut String) {
ret.push_str(&format!(
"Content-Transfer-Encoding: {}\n",
"Content-Transfer-Encoding: {}\r\n",
a.content_transfer_encoding
));
match &a.content_type {
@ -666,7 +699,7 @@ impl Attachment {
}
}
ret.push_str("\n\n");
ret.push_str("\r\n\r\n");
ret.push_str(&String::from_utf8_lossy(a.body()));
}
ContentType::Multipart {
@ -679,36 +712,36 @@ impl Attachment {
if *kind == MultipartType::Signed {
ret.push_str("; micalg=pgp-sha512; protocol=\"application/pgp-signature\"");
}
ret.push('\n');
ret.push_str("\r\n");
let boundary_start = format!("\n--{}\n", boundary);
let boundary_start = format!("\r\n--{}\r\n", boundary);
for p in parts {
ret.push_str(&boundary_start);
into_raw_helper(p, ret);
}
ret.push_str(&format!("--{}--\n\n", boundary));
ret.push_str(&format!("--{}--\r\n\r\n", boundary));
}
ContentType::MessageRfc822 => {
ret.push_str(&format!("Content-Type: {}\n\n", a.content_type));
ret.push_str(&format!("Content-Type: {}\r\n\r\n", a.content_type));
ret.push_str(&String::from_utf8_lossy(a.body()));
}
ContentType::PGPSignature => {
ret.push_str(&format!("Content-Type: {}\n\n", a.content_type));
ContentType::CMSSignature | ContentType::PGPSignature => {
ret.push_str(&format!("Content-Type: {}\r\n\r\n", a.content_type));
ret.push_str(&String::from_utf8_lossy(a.body()));
}
ContentType::OctetStream { ref name } => {
if let Some(name) = name {
ret.push_str(&format!(
"Content-Type: {}; name={}\n\n",
"Content-Type: {}; name={}\r\n\r\n",
a.content_type, name
));
} else {
ret.push_str(&format!("Content-Type: {}\n\n", a.content_type));
ret.push_str(&format!("Content-Type: {}\r\n\r\n", a.content_type));
}
ret.push_str(&BASE64_MIME.encode(a.body()).trim());
}
_ => {
ret.push_str(&format!("Content-Type: {}\n\n", a.content_type));
ret.push_str(&format!("Content-Type: {}\r\n\r\n", a.content_type));
ret.push_str(&String::from_utf8_lossy(a.body()));
}
}
@ -751,9 +784,18 @@ impl Attachment {
h.eq_ignore_ascii_case(b"name") | h.eq_ignore_ascii_case(b"filename")
})
.map(|(_, v)| String::from_utf8_lossy(v).to_string()),
ContentType::Other { name, .. } | ContentType::OctetStream { name, .. } => name.clone(),
ContentType::Other { .. } | ContentType::OctetStream { .. } => {
self.content_type.name().map(|s| s.to_string())
}
_ => None,
})
.map(|s| {
crate::email::parser::encodings::phrase(s.as_bytes(), false)
.map(|(_, v)| v)
.ok()
.and_then(|n| String::from_utf8(n).ok())
.unwrap_or_else(|| s)
})
.map(|n| n.replace(|c| std::path::is_separator(c) || c.is_ascii_control(), "_"))
}
}
@ -762,25 +804,24 @@ pub fn interpret_format_flowed(_t: &str) -> String {
unimplemented!()
}
fn decode_rfc822(_raw: &[u8]) -> Attachment {
// FIXME
let builder = AttachmentBuilder::new(b"message/rfc822 cannot be displayed");
builder.build()
}
type Filter<'a> = Box<dyn FnMut(&Attachment, &mut Vec<u8>) -> () + 'a>;
type Filter<'a> = Box<dyn FnMut(&'a Attachment, &mut Vec<u8>) -> () + 'a>;
fn decode_rec_helper<'a>(a: &'a Attachment, filter: &mut Option<Filter<'a>>) -> Vec<u8> {
fn decode_rec_helper<'a, 'b>(a: &'a Attachment, filter: &mut Option<Filter<'b>>) -> Vec<u8> {
match a.content_type {
ContentType::Other { .. } => Vec::new(),
ContentType::Text { .. } => decode_helper(a, filter),
ContentType::OctetStream { ref name } => {
name.clone().unwrap_or_else(|| a.mime_type()).into_bytes()
}
ContentType::PGPSignature => Vec::new(),
ContentType::CMSSignature | ContentType::PGPSignature => Vec::new(),
ContentType::MessageRfc822 => {
let temp = decode_rfc822(a.body());
decode_rec(&temp, None)
if a.content_disposition.kind.is_inline() {
let b = AttachmentBuilder::new(a.body()).build();
let ret = decode_rec_helper(&b, filter);
ret
} else {
b"message/rfc822 attachment".to_vec()
}
}
ContentType::Multipart {
ref kind,
@ -806,6 +847,16 @@ fn decode_rec_helper<'a>(a: &'a Attachment, filter: &mut Option<Filter<'a>>) ->
vec.extend(decode_helper(a, filter));
vec
}
MultipartType::Encrypted => {
let mut vec = Vec::new();
for a in parts {
if a.content_type == "application/octet-stream" {
vec.extend(decode_rec_helper(a, filter));
}
}
vec.extend(decode_helper(a, filter));
vec
}
_ => {
let mut vec = Vec::new();
for a in parts {
@ -819,11 +870,11 @@ fn decode_rec_helper<'a>(a: &'a Attachment, filter: &mut Option<Filter<'a>>) ->
}
}
pub fn decode_rec<'a>(a: &'a Attachment, mut filter: Option<Filter<'a>>) -> Vec<u8> {
pub fn decode_rec<'a, 'b>(a: &'a Attachment, mut filter: Option<Filter<'b>>) -> Vec<u8> {
decode_rec_helper(a, &mut filter)
}
fn decode_helper<'a>(a: &'a Attachment, filter: &mut Option<Filter<'a>>) -> Vec<u8> {
fn decode_helper<'a, 'b>(a: &'a Attachment, filter: &mut Option<Filter<'b>>) -> Vec<u8> {
let charset = match a.content_type {
ContentType::Text { charset: c, .. } => c,
_ => Default::default(),
@ -860,6 +911,6 @@ fn decode_helper<'a>(a: &'a Attachment, filter: &mut Option<Filter<'a>>) -> Vec<
ret
}
pub fn decode<'a>(a: &'a Attachment, mut filter: Option<Filter<'a>>) -> Vec<u8> {
pub fn decode<'a, 'b>(a: &'a Attachment, mut filter: Option<Filter<'b>>) -> Vec<u8> {
decode_helper(a, &mut filter)
}

View File

@ -19,6 +19,7 @@
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
/*! Compose a `Draft`, with MIME and attachment support */
use super::*;
use crate::email::attachment_types::{
Charset, ContentTransferEncoding, ContentType, MultipartType,
@ -52,7 +53,7 @@ impl Default for Draft {
let mut headers = HeaderMap::default();
headers.insert(
HeaderName::new_unchecked("Date"),
crate::datetime::timestamp_to_string(crate::datetime::now(), None),
crate::datetime::timestamp_to_string(crate::datetime::now(), None, true),
);
headers.insert(HeaderName::new_unchecked("From"), "".into());
headers.insert(HeaderName::new_unchecked("To"), "".into());
@ -220,7 +221,7 @@ impl Draft {
let mut ret = String::new();
for (k, v) in self.headers.deref() {
ret.extend(format!("{}: {}\n", k, v).chars());
ret.push_str(&format!("{}: {}\n", k, v));
}
ret.push('\n');
@ -245,9 +246,9 @@ impl Draft {
}
for (k, v) in self.headers.deref() {
if v.is_ascii() {
ret.extend(format!("{}: {}\r\n", k, v).chars());
ret.push_str(&format!("{}: {}\r\n", k, v));
} else {
ret.extend(format!("{}: {}\r\n", k, mime::encode_header(v)).chars());
ret.push_str(&format!("{}: {}\r\n", k, mime::encode_header(v)));
}
}
ret.push_str("MIME-Version: 1.0\r\n");
@ -255,19 +256,22 @@ impl Draft {
if self.attachments.is_empty() {
let content_type: ContentType = Default::default();
let content_transfer_encoding: ContentTransferEncoding = ContentTransferEncoding::_8Bit;
ret.extend(format!("Content-Type: {}; charset=\"utf-8\"\r\n", content_type).chars());
ret.extend(
format!(
"Content-Transfer-Encoding: {}\r\n",
content_transfer_encoding
)
.chars(),
);
ret.push_str(&format!(
"Content-Type: {}; charset=\"utf-8\"\r\n",
content_type
));
ret.push_str(&format!(
"Content-Transfer-Encoding: {}\r\n",
content_transfer_encoding
));
ret.push_str("\r\n");
ret.push_str(&self.body);
for line in self.body.lines() {
ret.push_str(line);
ret.push_str("\r\n");
}
} else if self.body.is_empty() && self.attachments.len() == 1 {
let attachment = std::mem::replace(&mut self.attachments, Vec::new()).remove(0);
print_attachment(&mut ret, &Default::default(), attachment);
print_attachment(&mut ret, attachment);
} else {
let mut parts = Vec::with_capacity(self.attachments.len() + 1);
let attachments = std::mem::replace(&mut self.attachments, Vec::new());
@ -286,28 +290,28 @@ impl Draft {
fn build_multipart(ret: &mut String, kind: MultipartType, parts: Vec<AttachmentBuilder>) {
let boundary = ContentType::make_boundary(&parts);
ret.extend(
format!(
"Content-Type: {}; charset=\"utf-8\"; boundary=\"{}\"\r\n",
kind, boundary
)
.chars(),
);
ret.push_str("\r\n");
ret.push_str(&format!(
r#"Content-Type: {}; charset="utf-8"; boundary="{}""#,
kind, boundary
));
if kind == MultipartType::Encrypted {
ret.push_str(r#"; protocol="application/pgp-encrypted""#);
}
ret.push_str("\r\n\r\n");
/* rfc1341 */
ret.push_str("This is a MIME formatted message with attachments. Use a MIME-compliant client to view it properly.\r\n");
for sub in parts {
ret.push_str("--");
ret.push_str(&boundary);
ret.push_str("\r\n");
print_attachment(ret, &kind, sub);
print_attachment(ret, sub);
}
ret.push_str("--");
ret.push_str(&boundary);
ret.push_str("--\n");
ret.push_str("--\r\n");
}
fn print_attachment(ret: &mut String, kind: &MultipartType, a: AttachmentBuilder) {
fn print_attachment(ret: &mut String, a: AttachmentBuilder) {
use ContentType::*;
match a.content_type {
ContentType::Text {
@ -316,63 +320,91 @@ fn print_attachment(ret: &mut String, kind: &MultipartType, a: AttachmentBuilder
parameters: ref v,
} if v.is_empty() => {
ret.push_str("\r\n");
ret.push_str(&String::from_utf8_lossy(a.raw()));
ret.push_str("\r\n");
for line in String::from_utf8_lossy(a.raw()).lines() {
ret.push_str(line);
ret.push_str("\r\n");
}
}
Text { .. } => {
ret.push_str(&a.build().into_raw());
ret.push_str("\r\n");
for line in a.build().into_raw().lines() {
ret.push_str(line);
ret.push_str("\r\n");
}
}
Multipart {
boundary: _boundary,
boundary: _,
kind,
parts: subparts,
parts,
} => {
build_multipart(
ret,
kind,
subparts
parts
.into_iter()
.map(|s| s.into())
.collect::<Vec<AttachmentBuilder>>(),
);
ret.push_str("\r\n");
}
MessageRfc822 | PGPSignature => {
ret.push_str(&format!("Content-Type: {}; charset=\"utf-8\"\r\n", kind));
MessageRfc822 => {
ret.push_str(&format!(
"Content-Type: {}; charset=\"utf-8\"\r\n",
a.content_type
));
ret.push_str("Content-Disposition: attachment\r\n");
ret.push_str("\r\n");
ret.push_str(&String::from_utf8_lossy(a.raw()));
for line in String::from_utf8_lossy(a.raw()).lines() {
ret.push_str(line);
ret.push_str("\r\n");
}
}
PGPSignature => {
ret.push_str(&format!(
"Content-Type: {}; charset=\"utf-8\"; name=\"signature.asc\"\r\n",
a.content_type
));
ret.push_str("Content-Description: Digital signature\r\n");
ret.push_str("Content-Disposition: inline\r\n");
ret.push_str("\r\n");
for line in String::from_utf8_lossy(a.raw()).lines() {
ret.push_str(line);
ret.push_str("\r\n");
}
}
_ => {
let content_transfer_encoding: ContentTransferEncoding =
ContentTransferEncoding::Base64;
if let Some(name) = a.content_type().name() {
ret.extend(
format!(
"Content-Type: {}; name=\"{}\"; charset=\"utf-8\"\r\n",
a.content_type(),
name
)
.chars(),
);
let content_transfer_encoding: ContentTransferEncoding = if a.raw().is_ascii() {
ContentTransferEncoding::_8Bit
} else {
ret.extend(
format!("Content-Type: {}; charset=\"utf-8\"\r\n", a.content_type()).chars(),
);
ContentTransferEncoding::Base64
};
if let Some(name) = a.content_type().name() {
ret.push_str(&format!(
"Content-Type: {}; name=\"{}\"; charset=\"utf-8\"\r\n",
a.content_type(),
name
));
} else {
ret.push_str(&format!(
"Content-Type: {}; charset=\"utf-8\"\r\n",
a.content_type()
));
}
ret.push_str("Content-Disposition: attachment\r\n");
ret.extend(
format!(
"Content-Transfer-Encoding: {}\r\n",
content_transfer_encoding
)
.chars(),
);
ret.push_str("\r\n");
ret.push_str(&BASE64_MIME.encode(a.raw()).trim());
ret.push_str(&format!(
"Content-Transfer-Encoding: {}\r\n",
content_transfer_encoding
));
ret.push_str("\r\n");
if content_transfer_encoding == ContentTransferEncoding::Base64 {
for line in BASE64_MIME.encode(a.raw()).trim().lines() {
ret.push_str(line);
ret.push_str("\r\n");
}
} else {
for line in String::from_utf8_lossy(a.raw()).lines() {
ret.push_str(line);
ret.push_str("\r\n");
}
}
}
}
}

View File

@ -179,7 +179,7 @@ fn test_encode_header() {
)
.unwrap(),
);
let words = "[Advcomparch] =?utf-8?b?zqPPhc68z4DOtc+BzrnPhs6/z4HOrCDPg861IGZs?=\n\t=?utf-8?b?dXNoIM67z4zOs8+JIG1pc3ByZWRpY3Rpb24gzrrOsc+Ezqwgz4TOt869?=\n\t=?utf-8?b?IM61zrrPhM6tzrvOtc+Dzrcgc3RvcmU=?=";
//let words = "[Advcomparch] =?utf-8?b?zqPPhc68z4DOtc+BzrnPhs6/z4HOrCDPg861IGZs?=\n\t=?utf-8?b?dXNoIM67z4zOs8+JIG1pc3ByZWRpY3Rpb24gzrrOsc+Ezqwgz4TOt869?=\n\t=?utf-8?b?IM61zrrPhM6tzrvOtc+Dzrcgc3RvcmU=?=";
let words_enc = "[Advcomparch] Συμπεριφορά σε flush λόγω misprediction κατά την εκτέλεση store";
assert_eq!(
"[Advcomparch] Συμπεριφορά σε flush λόγω misprediction κατά την εκτέλεση store",

View File

@ -19,6 +19,7 @@
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
/*! Wrapper type `HeaderName` for case-insensitive comparisons */
use crate::error::MeliError;
use indexmap::IndexMap;
use smallvec::SmallVec;

View File

@ -19,15 +19,18 @@
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
/*! Parsing of rfc2369/rfc2919 `List-*` headers */
use super::parser;
use super::Envelope;
use smallvec::SmallVec;
use std::convert::From;
#[derive(Debug, Copy)]
#[derive(Debug, PartialEq, Clone, Copy)]
pub enum ListAction<'a> {
Url(&'a [u8]),
Email(&'a [u8]),
///`List-Post` field may contain the special value "NO".
No,
}
impl<'a> From<&'a [u8]> for ListAction<'a> {
@ -37,6 +40,8 @@ impl<'a> From<&'a [u8]> for ListAction<'a> {
* parser::mailto() will handle this if user tries to unsubscribe.
*/
ListAction::Email(value)
} else if value.starts_with(b"NO") {
ListAction::No
} else {
/* Otherwise treat it as url. There's no foolproof way to check if this is valid, so
* postpone it until we try an HTTP request.
@ -68,15 +73,6 @@ impl<'a> ListAction<'a> {
}
}
impl<'a> Clone for ListAction<'a> {
fn clone(&self) -> Self {
match self {
ListAction::Url(a) => ListAction::Url(<&[u8]>::clone(a)),
ListAction::Email(a) => ListAction::Email(<&[u8]>::clone(a)),
}
}
}
#[derive(Default, Debug)]
pub struct ListActions<'a> {
pub id: Option<&'a str>,
@ -128,6 +124,11 @@ impl<'a> ListActions<'a> {
if let Some(post) = envelope.other_headers().get("List-Post") {
ret.post = ListAction::parse_options_list(post.as_bytes());
if let Some(ref l) = ret.post {
if l.starts_with(&[ListAction::No]) {
ret.post = None;
}
}
}
if let Some(unsubscribe) = envelope.other_headers().get("List-Unsubscribe") {

View File

@ -19,6 +19,7 @@
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
/*! Parsing of `mailto` addresses */
use super::*;
use std::convert::TryFrom;

View File

@ -19,6 +19,7 @@
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
/*! Parsers for email. See submodules */
use crate::error::{MeliError, Result, ResultIntoMeliError};
use nom::{
branch::alt,
@ -281,6 +282,7 @@ pub fn mail(input: &[u8]) -> Result<(Vec<(&[u8], &[u8])>, &[u8])> {
}
pub mod dates {
/*! Date values in headers */
use super::generic::*;
use super::*;
use crate::datetime::UnixTimestamp;
@ -505,6 +507,7 @@ pub mod dates {
}
pub mod generic {
/*! Generally useful parser combinators */
use super::*;
#[inline(always)]
pub fn byte_in_slice<'a>(slice: &'static [u8]) -> impl Fn(&'a [u8]) -> IResult<&'a [u8], u8> {
@ -1172,7 +1175,7 @@ pub mod mailing_lists {
map(tag("NO"), |_| ()),
map(opt(cfws), |_| ()),
),
|_| vec![],
|_| vec![&b"NO"[..]],
),
))(input)?;
let (input, _) = opt(cfws)(input)?;
@ -1197,14 +1200,15 @@ List-Archive: <http://www.host.com/list/archive/> (Web Archive)
"#;
let (rest, headers) = headers::headers(s.as_bytes()).unwrap();
assert!(rest.is_empty());
for (h, v) in headers {
let (rest, action_list) = rfc_2369_list_headers_action_list(v).unwrap();
for (_h, v) in headers {
let (rest, _action_list) = rfc_2369_list_headers_action_list(v).unwrap();
assert!(rest.is_empty());
}
}
}
pub mod headers {
/*! Email headers */
use super::*;
pub fn headers(input: &[u8]) -> IResult<&[u8], Vec<(&[u8], &[u8])>> {
@ -1465,6 +1469,7 @@ pub mod headers {
}
pub mod attachments {
/*! Email attachments */
use super::*;
use crate::email::address::*;
use crate::email::attachment_types::{ContentDisposition, ContentDispositionKind};
@ -1731,6 +1736,7 @@ pub mod attachments {
}
pub mod encodings {
/*! Email encodings (quoted printable, MIME) */
use super::*;
use crate::email::attachment_types::Charset;
use data_encoding::BASE64_MIME;
@ -1871,18 +1877,33 @@ pub mod encodings {
Charset::UTF8 | Charset::Ascii => Ok(String::from_utf8_lossy(s).to_string()),
Charset::ISO8859_1 => Ok(ISO_8859_1.decode(s, DecoderTrap::Strict)?),
Charset::ISO8859_2 => Ok(ISO_8859_2.decode(s, DecoderTrap::Strict)?),
Charset::ISO8859_3 => Ok(ISO_8859_3.decode(s, DecoderTrap::Strict)?),
Charset::ISO8859_4 => Ok(ISO_8859_4.decode(s, DecoderTrap::Strict)?),
Charset::ISO8859_5 => Ok(ISO_8859_5.decode(s, DecoderTrap::Strict)?),
Charset::ISO8859_6 => Ok(ISO_8859_6.decode(s, DecoderTrap::Strict)?),
Charset::ISO8859_7 => Ok(ISO_8859_7.decode(s, DecoderTrap::Strict)?),
Charset::ISO8859_8 => Ok(ISO_8859_8.decode(s, DecoderTrap::Strict)?),
Charset::ISO8859_10 => Ok(ISO_8859_10.decode(s, DecoderTrap::Strict)?),
Charset::ISO8859_13 => Ok(ISO_8859_13.decode(s, DecoderTrap::Strict)?),
Charset::ISO8859_14 => Ok(ISO_8859_14.decode(s, DecoderTrap::Strict)?),
Charset::ISO8859_15 => Ok(ISO_8859_15.decode(s, DecoderTrap::Strict)?),
Charset::ISO8859_16 => Ok(ISO_8859_16.decode(s, DecoderTrap::Strict)?),
Charset::GBK => Ok(GBK.decode(s, DecoderTrap::Strict)?),
Charset::Windows1250 => Ok(WINDOWS_1250.decode(s, DecoderTrap::Strict)?),
Charset::Windows1251 => Ok(WINDOWS_1251.decode(s, DecoderTrap::Strict)?),
Charset::Windows1252 => Ok(WINDOWS_1252.decode(s, DecoderTrap::Strict)?),
Charset::Windows1253 => Ok(WINDOWS_1253.decode(s, DecoderTrap::Strict)?),
// Unimplemented:
Charset::GB2312 => Ok(String::from_utf8_lossy(s).to_string()),
Charset::UTF16 => Ok(String::from_utf8_lossy(s).to_string()),
Charset::BIG5 => Ok(String::from_utf8_lossy(s).to_string()),
Charset::ISO2022JP => Ok(String::from_utf8_lossy(s).to_string()),
Charset::KOI8R => Ok(KOI8_R.decode(s, DecoderTrap::Strict)?),
Charset::KOI8U => Ok(KOI8_U.decode(s, DecoderTrap::Strict)?),
Charset::BIG5 => Ok(BIG5_2003.decode(s, DecoderTrap::Strict)?),
Charset::GB2312 => {
Ok(encoding::codec::simpchinese::GBK_ENCODING.decode(s, DecoderTrap::Strict)?)
}
Charset::UTF16 => {
Ok(encoding::codec::utf_16::UTF_16LE_ENCODING.decode(s, DecoderTrap::Strict)?)
}
Charset::ISO2022JP => Ok(ISO_2022_JP.decode(s, DecoderTrap::Strict)?),
Charset::EUCJP => Ok(EUC_JP.decode(s, DecoderTrap::Strict)?),
}
}
@ -2465,7 +2486,7 @@ pub mod address {
#[cfg(test)]
mod tests {
use super::{address::*, encodings::*, generic::*, *};
use super::{address::*, encodings::*, *};
use crate::email::address::*;
use crate::make_address;

View File

@ -19,14 +19,17 @@
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
use crate::email::parser::BytesExt;
/*! Verification of OpenPGP signatures */
use crate::email::{
attachment_types::{ContentType, MultipartType},
attachments::Attachment,
};
use crate::{MeliError, Result};
/// rfc3156
/// Convert raw attachment to the form needed for signature verification ([rfc3156](https://tools.ietf.org/html/rfc3156))
///
/// ## rfc3156
/// ```text
/// Upon receipt of a signed message, an application MUST:
///
/// (1) Convert line endings to the canonical <CR><LF> sequence before
@ -35,7 +38,7 @@ use crate::{MeliError, Result};
/// (2) Pass both the signed data and its associated content headers
/// along with the OpenPGP signature to the signature verification
/// service.
///
/// ```
pub fn convert_attachment_to_rfc_spec(input: &[u8]) -> Vec<u8> {
if input.is_empty() {
return Vec::new();
@ -84,7 +87,7 @@ pub fn convert_attachment_to_rfc_spec(input: &[u8]) -> Vec<u8> {
ret
}
pub fn verify_signature(a: &Attachment) -> Result<(Vec<u8>, &[u8])> {
pub fn verify_signature(a: &Attachment) -> Result<(Vec<u8>, &Attachment)> {
match a.content_type {
ContentType::Multipart {
kind: MultipartType::Signed,
@ -103,7 +106,10 @@ pub fn verify_signature(a: &Attachment) -> Result<(Vec<u8>, &[u8])> {
let signed_part: Vec<u8> = if let Some(v) = parts
.iter()
.zip(part_boundaries.iter())
.find(|(p, _)| p.content_type != ContentType::PGPSignature)
.find(|(p, _)| {
p.content_type != ContentType::PGPSignature
&& p.content_type != ContentType::CMSSignature
})
.map(|(_, s)| convert_attachment_to_rfc_spec(s.display_bytes(a.body())))
{
v
@ -112,12 +118,11 @@ pub fn verify_signature(a: &Attachment) -> Result<(Vec<u8>, &[u8])> {
"multipart/signed attachment without a signed part".to_string(),
));
};
let signature = if let Some(sig) = parts
.iter()
.find(|s| s.content_type == ContentType::PGPSignature)
.map(|a| a.body())
{
sig.trim()
let signature = if let Some(sig) = parts.iter().find(|s| {
s.content_type == ContentType::PGPSignature
|| s.content_type == ContentType::CMSSignature
}) {
sig
} else {
return Err(MeliError::new(
"multipart/signed attachment without a signature part".to_string(),
@ -125,8 +130,29 @@ pub fn verify_signature(a: &Attachment) -> Result<(Vec<u8>, &[u8])> {
};
Ok((signed_part, signature))
}
_ => {
unreachable!("Should not give non-signed attachments to this function");
}
_ => Err(MeliError::new(
"Should not give non-signed attachments to this function",
)),
}
}
#[derive(Debug, Clone, Default)]
pub struct DecryptionMetadata {
pub recipients: Vec<Recipient>,
pub file_name: Option<String>,
pub session_key: Option<String>,
pub is_mime: bool,
}
#[derive(Debug, Clone)]
pub struct Recipient {
pub keyid: Option<String>,
pub status: Result<()>,
}
#[derive(Debug, Clone, Default)]
pub struct SignatureMetadata {
pub signatures: Vec<Recipient>,
pub file_name: Option<String>,
pub is_mime: bool,
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,155 @@
/*
* melib - gpgme module
*
* Copyright 2020 Manos Pitsidianakis
*
* This file is part of meli.
*
* meli is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* meli is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
use super::*;
use std::io::{self, Read, Seek, Write};
#[repr(C)]
struct TagData {
idx: usize,
fd: ::std::os::raw::c_int,
io_state: Arc<Mutex<IoState>>,
}
pub unsafe extern "C" fn gpgme_register_io_cb(
data: *mut ::std::os::raw::c_void,
fd: ::std::os::raw::c_int,
dir: ::std::os::raw::c_int,
fnc: gpgme_io_cb_t,
fnc_data: *mut ::std::os::raw::c_void,
tag: *mut *mut ::std::os::raw::c_void,
) -> gpgme_error_t {
let io_state: Arc<Mutex<IoState>> = Arc::from_raw(data as *const _);
let io_state_copy = io_state.clone();
let mut io_state_lck = io_state.lock().unwrap();
let idx = io_state_lck.max_idx;
io_state_lck.max_idx += 1;
let (sender, receiver) = smol::channel::unbounded();
let gpgfd = GpgmeFd {
fd,
fnc,
fnc_data,
idx,
write: dir == 0,
sender,
receiver,
io_state: io_state_copy.clone(),
};
let tag_data = Arc::into_raw(Arc::new(TagData {
idx,
fd,
io_state: io_state_copy,
}));
core::ptr::write(tag, tag_data as *mut _);
io_state_lck.ops.insert(idx, gpgfd);
drop(io_state_lck);
let _ = Arc::into_raw(io_state);
0
}
pub unsafe extern "C" fn gpgme_remove_io_cb(tag: *mut ::std::os::raw::c_void) {
let tag_data: Arc<TagData> = Arc::from_raw(tag as *const _);
let mut io_state_lck = tag_data.io_state.lock().unwrap();
let fd = io_state_lck.ops.remove(&tag_data.idx).unwrap();
fd.sender.try_send(()).unwrap();
drop(io_state_lck);
let _ = Arc::into_raw(tag_data);
}
pub unsafe extern "C" fn gpgme_event_io_cb(
data: *mut ::std::os::raw::c_void,
type_: gpgme_event_io_t,
type_data: *mut ::std::os::raw::c_void,
) {
if type_ == gpgme_event_io_t_GPGME_EVENT_DONE {
let err = type_data as gpgme_io_event_done_data_t;
let io_state: Arc<Mutex<IoState>> = Arc::from_raw(data as *const _);
let io_state_lck = io_state.lock().unwrap();
io_state_lck.sender.try_send(()).unwrap();
*io_state_lck.done.lock().unwrap() = Some(gpgme_error_try(&io_state_lck.lib, (*err).err));
drop(io_state_lck);
let _ = Arc::into_raw(io_state);
} else if type_ == gpgme_event_io_t_GPGME_EVENT_NEXT_KEY {
if let Some(inner) = core::ptr::NonNull::new(type_data as gpgme_key_t) {
let io_state: Arc<Mutex<IoState>> = Arc::from_raw(data as *const _);
let io_state_lck = io_state.lock().unwrap();
io_state_lck
.key_sender
.try_send(KeyInner { inner })
.unwrap();
drop(io_state_lck);
let _ = Arc::into_raw(io_state);
}
}
}
impl Read for Data {
#[inline]
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
let result = unsafe {
let (buf, len) = (buf.as_mut_ptr() as *mut _, buf.len());
call!(self.lib, gpgme_data_read)(self.inner.as_ptr(), buf, len)
};
if result >= 0 {
Ok(result as usize)
} else {
Err(io::Error::last_os_error().into())
}
}
}
impl Write for Data {
#[inline]
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
let result = unsafe {
let (buf, len) = (buf.as_ptr() as *const _, buf.len());
call!(self.lib, gpgme_data_write)(self.inner.as_ptr(), buf, len)
};
if result >= 0 {
Ok(result as usize)
} else {
Err(io::Error::last_os_error().into())
}
}
#[inline]
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
impl Seek for Data {
#[inline]
fn seek(&mut self, pos: io::SeekFrom) -> io::Result<u64> {
use std::convert::TryInto;
let (off, whence) = match pos {
io::SeekFrom::Start(off) => (off.try_into().unwrap_or(i64::MAX), libc::SEEK_SET),
io::SeekFrom::End(off) => (off.saturating_abs(), libc::SEEK_END),
io::SeekFrom::Current(off) => (off, libc::SEEK_CUR),
};
let result = unsafe { call!(self.lib, gpgme_data_seek)(self.inner.as_ptr(), off, whence) };
if result >= 0 {
Ok(result as u64)
} else {
Err(io::Error::last_os_error().into())
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -41,7 +41,11 @@ pub mod dbg {
() => {
eprint!(
"[{}][{:?}] {}:{}_{}: ",
crate::datetime::timestamp_to_string(crate::datetime::now(), Some("%Y-%m-%d %T")),
crate::datetime::timestamp_to_string(
crate::datetime::now(),
Some("%Y-%m-%d %T"),
false
),
std::thread::current()
.name()
.map(std::string::ToString::to_string)
@ -119,6 +123,8 @@ pub mod connections;
pub mod parsec;
pub mod search;
#[cfg(feature = "gpgme")]
pub mod gpgme;
#[cfg(feature = "smtp")]
pub mod smtp;
#[cfg(feature = "sqlite3")]
@ -140,6 +146,35 @@ pub extern crate smol;
pub extern crate uuid;
pub extern crate xdg_utils;
#[derive(Debug, Copy, Clone)]
pub struct Bytes(pub usize);
impl Bytes {
pub const KILOBYTE: f64 = 1024.0;
pub const MEGABYTE: f64 = Self::KILOBYTE * 1024.0;
pub const GIGABYTE: f64 = Self::MEGABYTE * 1024.0;
pub const PETABYTE: f64 = Self::GIGABYTE * 1024.0;
}
impl core::fmt::Display for Bytes {
fn fmt(&self, fmt: &mut core::fmt::Formatter) -> core::fmt::Result {
let bytes: f64 = self.0 as f64;
if bytes == 0.0 {
write!(fmt, "0")
} else if bytes < Self::KILOBYTE {
write!(fmt, "{:.2} bytes", bytes)
} else if bytes < Self::MEGABYTE {
write!(fmt, "{:.2} KiB", bytes / Self::KILOBYTE)
} else if bytes < Self::GIGABYTE {
write!(fmt, "{:.2} MiB", bytes / Self::MEGABYTE)
} else if bytes < Self::PETABYTE {
write!(fmt, "{:.2} GiB", bytes / Self::GIGABYTE)
} else {
write!(fmt, "{:.2} PiB", bytes / Self::PETABYTE)
}
}
}
pub use shellexpand::ShellExpandTrait;
pub mod shellexpand {

View File

@ -85,7 +85,8 @@ pub fn log<S: AsRef<str>>(val: S, level: LoggingLevel) {
if level <= b.level {
b.dest
.write_all(
crate::datetime::timestamp_to_string(crate::datetime::now(), None).as_bytes(),
crate::datetime::timestamp_to_string(crate::datetime::now(), None, false)
.as_bytes(),
)
.unwrap();
b.dest.write_all(b" [").unwrap();

View File

@ -50,7 +50,11 @@
* require_auth: true,
* },
*};
*std::thread::spawn(|| smol::run(futures::future::pending::<()>()));
*
*std::thread::Builder::new().spawn(move || {
* let ex = smol::Executor::new();
* futures::executor::block_on(ex.run(futures::future::pending::<()>()));
*}).unwrap();
*
*let mut conn = futures::executor::block_on(SmtpConnection::new_connection(conf)).unwrap();
*futures::executor::block_on(conn.mail_transaction(r#"To: l10@mail.gr
@ -73,7 +77,7 @@ use crate::error::{MeliError, Result, ResultIntoMeliError};
use futures::io::{AsyncReadExt, AsyncWriteExt};
use native_tls::TlsConnector;
use smallvec::SmallVec;
use smol::blocking;
use smol::unblock;
use smol::Async as AsyncWrapper;
use std::borrow::Cow;
use std::convert::TryFrom;
@ -133,10 +137,24 @@ pub enum SmtpAuth {
password: Password,
#[serde(default = "true_val")]
require_auth: bool,
#[serde(skip_serializing, skip_deserializing, default)]
auth_type: SmtpAuthType,
},
#[serde(alias = "xoauth2")]
XOAuth2 {
token_command: String,
#[serde(default = "true_val")]
require_auth: bool,
},
// md5, sasl, etc
}
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Default)]
pub struct SmtpAuthType {
plain: bool,
login: bool,
}
fn true_val() -> bool {
true
}
@ -150,7 +168,7 @@ impl SmtpAuth {
use SmtpAuth::*;
match self {
None => false,
Auto { require_auth, .. } => *require_auth,
Auto { require_auth, .. } | XOAuth2 { require_auth, .. } => *require_auth,
}
}
}
@ -310,7 +328,9 @@ impl SmtpConnection {
let _path = path.clone();
socket.set_nonblocking(false)?;
let conn_result = blocking!(connector.connect(&_path, socket));
let conn = unblock(move || connector.connect(&_path, socket))
.await
.chain_err_kind(crate::error::ErrorKind::Network)?;
/*
if let Err(native_tls::HandshakeError::WouldBlock(midhandshake_stream)) =
conn_result
@ -332,10 +352,8 @@ impl SmtpConnection {
}
}
*/
AsyncWrapper::new(Connection::Tls(
conn_result.chain_err_kind(crate::error::ErrorKind::Network)?,
))
.chain_err_kind(crate::error::ErrorKind::Network)?
AsyncWrapper::new(Connection::Tls(conn))
.chain_err_kind(crate::error::ErrorKind::Network)?
};
ret.write_all(b"EHLO meli.delivery\r\n")
.await
@ -392,38 +410,56 @@ impl SmtpConnection {
return Err(MeliError::new(format!(
"SMTP Server doesn't advertise Authentication support. Server response was: {:?}",
pre_auth_extensions_reply
)));
)).set_kind(crate::error::ErrorKind::Authentication));
}
no_auth_needed =
ret.server_conf.auth == SmtpAuth::None || !ret.server_conf.auth.require_auth();
if no_auth_needed {
ret.set_extension_support(pre_auth_extensions_reply);
} else if let SmtpAuth::Auto {
ref mut auth_type, ..
} = ret.server_conf.auth
{
for l in pre_auth_extensions_reply
.lines
.iter()
.filter(|l| l.starts_with("AUTH"))
{
let l = l["AUTH ".len()..].trim();
for _type in l.split_whitespace() {
if _type == "PLAIN" {
auth_type.plain = true;
} else if _type == "LOGIN" {
auth_type.login = true;
}
}
break;
}
}
}
if !no_auth_needed {
match &ret.server_conf.auth {
SmtpAuth::None => {}
SmtpAuth::Auto {
username, password, ..
username,
password,
auth_type,
..
} => {
// # RFC 4616 The PLAIN SASL Mechanism
// # https://www.ietf.org/rfc/rfc4616.txt
// message = [authzid] UTF8NUL authcid UTF8NUL passwd
// authcid = 1*SAFE ; MUST accept up to 255 octets
// authzid = 1*SAFE ; MUST accept up to 255 octets
// passwd = 1*SAFE ; MUST accept up to 255 octets
// UTF8NUL = %x00 ; UTF-8 encoded NUL character
let username_password = match password {
Password::Raw(p) => base64::encode(format!("\0{}\0{}", username, p)),
let password = match password {
Password::Raw(p) => p.as_bytes().to_vec(),
Password::CommandEval(command) => {
let _command = command.clone();
let mut output = blocking!(Command::new("sh")
.args(&["-c", &_command])
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.output())?;
let mut output = unblock(move || {
Command::new("sh")
.args(&["-c", &_command])
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.output()
})
.await?;
if !output.status.success() {
return Err(MeliError::new(format!(
"SMTP password evaluation command `{}` returned {}: {}",
@ -432,22 +468,77 @@ impl SmtpConnection {
String::from_utf8_lossy(&output.stderr)
)));
}
let mut buf =
Vec::with_capacity(2 + username.len() + output.stdout.len());
buf.push(b'\0');
buf.extend(username.as_bytes().to_vec());
buf.push(b'\0');
if output.stdout.ends_with(b"\n") {
output.stdout.pop();
}
buf.extend(output.stdout);
base64::encode(buf)
output.stdout
}
};
let mut auth_command: SmallVec<[&[u8]; 16]> = SmallVec::new();
auth_command.push(b"AUTH PLAIN ");
auth_command.push(username_password.as_bytes());
ret.send_command(&auth_command).await?;
if auth_type.login {
let username = username.to_string();
ret.send_command(&[b"AUTH LOGIN"]).await?;
ret.read_lines(&mut res, Some((ReplyCode::_334, &[])))
.await
.chain_err_kind(crate::error::ErrorKind::Authentication)?;
let buf = base64::encode(&username);
ret.send_command(&[buf.as_bytes()]).await?;
ret.read_lines(&mut res, Some((ReplyCode::_334, &[])))
.await
.chain_err_kind(crate::error::ErrorKind::Authentication)?;
let buf = base64::encode(&password);
ret.send_command(&[buf.as_bytes()]).await?;
} else {
// # RFC 4616 The PLAIN SASL Mechanism
// # https://www.ietf.org/rfc/rfc4616.txt
// message = [authzid] UTF8NUL authcid UTF8NUL passwd
// authcid = 1*SAFE ; MUST accept up to 255 octets
// authzid = 1*SAFE ; MUST accept up to 255 octets
// passwd = 1*SAFE ; MUST accept up to 255 octets
// UTF8NUL = %x00 ; UTF-8 encoded NUL character
let username_password = {
let mut buf = Vec::with_capacity(2 + username.len() + password.len());
buf.push(b'\0');
buf.extend(username.as_bytes().to_vec());
buf.push(b'\0');
buf.extend(password);
base64::encode(buf)
};
ret.send_command(&[b"AUTH PLAIN ", username_password.as_bytes()])
.await?;
}
ret.read_lines(&mut res, Some((ReplyCode::_235, &[])))
.await
.chain_err_kind(crate::error::ErrorKind::Authentication)?;
ret.send_command(&[b"EHLO meli.delivery"]).await?;
}
SmtpAuth::XOAuth2 { token_command, .. } => {
let password_token = {
let _token_command = token_command.clone();
let mut output = unblock(move || {
Command::new("sh")
.args(&["-c", &_token_command])
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.output()
})
.await?;
if !output.status.success() {
return Err(MeliError::new(format!(
"SMTP XOAUTH2 token evaluation command `{}` returned {}: {}",
&token_command,
output.status,
String::from_utf8_lossy(&output.stderr)
)));
}
if output.stdout.ends_with(b"\n") {
output.stdout.pop();
}
output.stdout
};
// https://developers.google.com/gmail/imap/xoauth2-protocol#smtp_protocol_exchange
ret.send_command(&[b"AUTH XOAUTH2 ", &password_token])
.await?;
ret.read_lines(&mut res, Some((ReplyCode::_235, &[])))
.await
.chain_err_kind(crate::error::ErrorKind::Authentication)?;
@ -565,7 +656,7 @@ impl SmtpConnection {
current_command.push(b"RCPT TO:<");
current_command.push(addr.address_spec_raw().trim());
if let Some(dsn_notify) = dsn_notify.as_ref() {
current_command.push(b" NOTIFY=");
current_command.push(b"> NOTIFY=");
current_command.push(dsn_notify.as_bytes());
} else {
current_command.push(b">");
@ -704,6 +795,8 @@ pub enum ReplyCode {
_251,
///Cannot VRFY user, but will accept message and attempt delivery (See Section 3.5.3)
_252,
///rfc4954 AUTH continuation request
_334,
///PRDR specific, eg "content analysis has started|
_353,
///Start mail input; end with <CRLF>.<CRLF>
@ -758,6 +851,7 @@ impl ReplyCode {
_235 => "Authentication successful",
_251 => "User not local; will forward",
_252 => "Cannot VRFY user, but will accept message and attempt delivery",
_334 => "Intermediate response to the AUTH command",
_353 => "PRDR specific notice",
_354 => "Start mail input; end with <CRLF>.<CRLF>",
_421 => "Service not available, closing transmission channel",
@ -808,6 +902,7 @@ impl TryFrom<&'_ str> for ReplyCode {
"250" => Ok(_250),
"251" => Ok(_251),
"252" => Ok(_252),
"334" => Ok(_334),
"354" => Ok(_354),
"421" => Ok(_421),
"450" => Ok(_450),
@ -910,6 +1005,9 @@ async fn read_lines<'r>(
}
}
}
if ret.len() < 3 {
return Err(MeliError::new(format!("Invalid SMTP reply: {}", ret)));
}
let code = ReplyCode::try_from(&ret[..3])?;
let reply = Reply::new(ret, code);
//debug!(&reply);

View File

@ -50,50 +50,65 @@ pub fn open_or_create_db(
description: &DatabaseDescription,
identifier: Option<&str>,
) -> Result<Connection> {
let db_path = if let Some(id) = identifier {
db_path(&format!("{}_{}", id, description.name))
} else {
db_path(description.name)
}?;
let mut set_mode = false;
if !db_path.exists() {
log(
format!(
"Creating {} database in {}",
description.name,
db_path.display()
),
crate::INFO,
);
set_mode = true;
}
let conn = Connection::open(&db_path).map_err(|e| MeliError::new(e.to_string()))?;
if set_mode {
use std::os::unix::fs::PermissionsExt;
let file = std::fs::File::open(&db_path)?;
let metadata = file.metadata()?;
let mut permissions = metadata.permissions();
let mut second_try: bool = false;
loop {
let db_path = if let Some(id) = identifier {
db_path(&format!("{}_{}", id, description.name))
} else {
db_path(description.name)
}?;
let mut set_mode = false;
if !db_path.exists() {
log(
format!(
"Creating {} database in {}",
description.name,
db_path.display()
),
crate::INFO,
);
set_mode = true;
}
let conn = Connection::open(&db_path).map_err(|e| MeliError::new(e.to_string()))?;
if set_mode {
use std::os::unix::fs::PermissionsExt;
let file = std::fs::File::open(&db_path)?;
let metadata = file.metadata()?;
let mut permissions = metadata.permissions();
permissions.set_mode(0o600); // Read/write for owner only.
file.set_permissions(permissions)?;
}
let version: i32 = conn.pragma_query_value(None, "user_version", |row| row.get(0))?;
if version != 0_i32 && version as u32 != description.version {
return Err(MeliError::new(format!(
"Database version mismatch, is {} but expected {}",
version, description.version
)));
}
permissions.set_mode(0o600); // Read/write for owner only.
file.set_permissions(permissions)?;
}
let version: i32 = conn.pragma_query_value(None, "user_version", |row| row.get(0))?;
if version != 0_i32 && version as u32 != description.version {
log(
format!(
"Database version mismatch, is {} but expected {}",
version, description.version
),
crate::INFO,
);
if second_try {
return Err(MeliError::new(format!(
"Database version mismatch, is {} but expected {}. Could not recreate database.",
version, description.version
)));
}
reset_db(description, identifier)?;
second_try = true;
continue;
}
if version == 0 {
conn.pragma_update(None, "user_version", &description.version)?;
}
if let Some(s) = description.init_script {
conn.execute_batch(s)
.map_err(|e| MeliError::new(e.to_string()))?;
}
if version == 0 {
conn.pragma_update(None, "user_version", &description.version)?;
}
if let Some(s) = description.init_script {
conn.execute_batch(s)
.map_err(|e| MeliError::new(e.to_string()))?;
}
Ok(conn)
return Ok(conn);
}
}
/// Return database to a clean slate.
@ -120,17 +135,27 @@ pub fn reset_db(description: &DatabaseDescription, identifier: Option<&str>) ->
impl ToSql for Envelope {
fn to_sql(&self) -> rusqlite::Result<ToSqlOutput> {
let v: Vec<u8> = bincode::serialize(self).map_err(|e| {
rusqlite::Error::ToSqlConversionFailure(Box::new(MeliError::new(e.to_string())))
})?;
let v: Vec<u8> = bincode::Options::serialize(bincode::config::DefaultOptions::new(), self)
.map_err(|e| {
rusqlite::Error::ToSqlConversionFailure(Box::new(MeliError::new(e.to_string())))
})?;
Ok(ToSqlOutput::from(v))
}
}
impl FromSql for Envelope {
fn column_result(value: rusqlite::types::ValueRef) -> FromSqlResult<Self> {
use std::convert::TryFrom;
let b: Vec<u8> = FromSql::column_result(value)?;
Ok(bincode::deserialize(&b)
.map_err(|e| FromSqlError::Other(Box::new(MeliError::new(e.to_string()))))?)
Ok(bincode::Options::deserialize(
bincode::Options::with_limit(
bincode::config::DefaultOptions::new(),
2 * u64::try_from(b.len()).map_err(|e| FromSqlError::Other(Box::new(e)))?,
),
&b,
)
.map_err(|e| FromSqlError::Other(Box::new(e)))?)
}
}

View File

@ -28,6 +28,7 @@ use super::types::Reflow;
use core::cmp::Ordering;
use core::iter::Peekable;
use core::str::FromStr;
use std::collections::VecDeque;
use LineBreakClass::*;
#[derive(Debug, PartialEq, Copy, Clone)]
@ -1102,12 +1103,23 @@ pub fn split_lines_reflow(text: &str, reflow: Reflow, width: Option<usize>) -> V
split(&mut ret, line, width);
continue;
}
let segment_tree = {
use std::iter::FromIterator;
let mut t: smallvec::SmallVec<[usize; 1024]> =
smallvec::SmallVec::from_iter(std::iter::repeat(0).take(line.len()));
for (idx, _g) in UnicodeSegmentation::grapheme_indices(line, true) {
t[idx] = 1;
}
segment_tree::SegmentTree::new(t)
};
let mut prev = 0;
let mut prev_line_offset = 0;
while prev < breaks.len() {
let new_off = match breaks[prev..].binary_search_by(|(offset, _)| {
line[prev_line_offset..*offset].grapheme_len().cmp(&width)
segment_tree
.get_sum(prev_line_offset, offset.saturating_sub(1))
.cmp(&width)
}) {
Ok(v) => v,
Err(v) => v,
@ -1233,3 +1245,554 @@ easy to take MORE than nothing.'"#;
println!("{}", l);
}
}
mod segment_tree {
/*! Simple segment tree implementation for maximum in range queries. This is useful if given an
* array of numbers you want to get the maximum value inside an interval quickly.
*/
use smallvec::SmallVec;
use std::convert::TryFrom;
use std::iter::FromIterator;
#[derive(Default, Debug, Clone)]
pub(super) struct SegmentTree {
array: SmallVec<[usize; 1024]>,
tree: SmallVec<[usize; 1024]>,
}
impl SegmentTree {
pub(super) fn new(val: SmallVec<[usize; 1024]>) -> SegmentTree {
if val.is_empty() {
return SegmentTree {
array: val.clone(),
tree: val,
};
}
let height = (f64::from(u32::try_from(val.len()).unwrap_or(0)))
.log2()
.ceil() as u32;
let max_size = 2 * (2_usize.pow(height));
let mut segment_tree: SmallVec<[usize; 1024]> =
SmallVec::from_iter(core::iter::repeat(0).take(max_size));
for i in 0..val.len() {
segment_tree[val.len() + i] = val[i];
}
for i in (1..val.len()).rev() {
segment_tree[i] = segment_tree[2 * i] + segment_tree[2 * i + 1];
}
SegmentTree {
array: val,
tree: segment_tree,
}
}
/// (left, right) is inclusive
pub(super) fn get_sum(&self, mut left: usize, mut right: usize) -> usize {
if self.array.is_empty() {
return 0;
}
let len = self.array.len();
if left > right {
return 0;
}
if right >= len {
right = len.saturating_sub(1);
}
left += len;
right += len + 1;
let mut sum = 0;
while left < right {
if (left & 1) > 0 {
sum += self.tree[left];
left += 1;
}
if (right & 1) > 0 {
right -= 1;
sum += self.tree[right];
}
left /= 2;
right /= 2;
}
sum
}
}
}
/// A lazy stateful iterator for line breaking text. Useful for very long text where you don't want
/// to linebreak it completely before user requests specific lines.
#[derive(Debug, Clone)]
pub struct LineBreakText {
text: String,
reflow: Reflow,
paragraph: VecDeque<String>,
paragraph_start_index: usize,
width: Option<usize>,
state: ReflowState,
}
#[derive(Debug, Clone)]
enum ReflowState {
ReflowNo {
cur_index: usize,
},
ReflowAllWidth {
width: usize,
state: LineBreakTextState,
},
ReflowAll {
cur_index: usize,
},
ReflowFormatFlowed {
cur_index: usize,
},
}
impl ReflowState {
fn new(reflow: Reflow, width: Option<usize>, cur_index: usize) -> ReflowState {
match reflow {
Reflow::All if width.is_some() => ReflowState::ReflowAllWidth {
width: width.unwrap(),
state: LineBreakTextState::AtLine { cur_index },
},
Reflow::All => ReflowState::ReflowAll { cur_index },
Reflow::FormatFlowed => ReflowState::ReflowFormatFlowed { cur_index },
Reflow::No => ReflowState::ReflowNo { cur_index },
}
}
}
#[derive(Debug, Clone)]
enum LineBreakTextState {
AtLine {
cur_index: usize,
},
WithinLine {
line_index: usize,
line_length: usize,
within_line_index: usize,
breaks: Vec<(usize, LineBreakCandidate)>,
prev_break: usize,
segment_tree: segment_tree::SegmentTree,
},
}
impl Default for LineBreakText {
fn default() -> Self {
Self::new(String::new(), Reflow::default(), None)
}
}
impl LineBreakText {
pub fn new(text: String, reflow: Reflow, width: Option<usize>) -> Self {
LineBreakText {
text,
state: ReflowState::new(reflow, width, 0),
paragraph: VecDeque::new(),
paragraph_start_index: 0,
reflow,
width,
}
}
pub fn width(&self) -> Option<usize> {
self.width
}
pub fn set_reflow(&mut self, new_val: Reflow) -> &mut Self {
self.reflow = new_val;
self.paragraph.clear();
self.state = ReflowState::new(self.reflow, self.width, self.paragraph_start_index);
self
}
pub fn set_width(&mut self, new_val: Option<usize>) -> &mut Self {
self.width = new_val;
self.paragraph.clear();
self.state = ReflowState::new(self.reflow, self.width, self.paragraph_start_index);
self
}
pub fn set_text(&mut self, new_val: String) -> &mut Self {
self.text = new_val;
self.reset()
}
pub fn reset(&mut self) -> &mut Self {
self.paragraph.clear();
self.state = ReflowState::new(self.reflow, self.width, 0);
self.paragraph_start_index = 0;
self
}
pub fn is_finished(&self) -> bool {
match self.state {
ReflowState::ReflowNo { cur_index }
| ReflowState::ReflowAll { cur_index }
| ReflowState::ReflowFormatFlowed { cur_index }
| ReflowState::ReflowAllWidth {
width: _,
state: LineBreakTextState::AtLine { cur_index },
} => cur_index >= self.text.len(),
ReflowState::ReflowAllWidth {
width: _,
state: LineBreakTextState::WithinLine { .. },
} => false,
}
}
}
impl Iterator for LineBreakText {
type Item = String;
fn next(&mut self) -> Option<Self::Item> {
if !self.paragraph.is_empty() {
return self.paragraph.pop_front();
}
if self.is_finished() {
return None;
}
match self.state {
ReflowState::ReflowFormatFlowed { ref mut cur_index } => {
/* rfc3676 - The Text/Plain Format and DelSp Parameters
* https://tools.ietf.org/html/rfc3676 */
/*
* - Split lines with indices using str::match_indices()
* - Iterate and reflow flow regions, and pass fixed regions through
*/
self.paragraph_start_index = *cur_index;
let line_indices_iter = self.text[*cur_index..].match_indices('\n').map(|(i, _)| i);
let start_offset = *cur_index;
let mut prev_index = *cur_index;
let mut in_paragraph = false;
let mut paragraph_start = *cur_index;
let mut prev_quote_depth = 0;
let mut paragraph = VecDeque::new();
for i in line_indices_iter {
let i = i + start_offset + 1;
let line = &self.text[prev_index..i];
let mut trimmed = line.trim_start().lines().next().unwrap_or("");
let mut quote_depth = 0;
let p_str: usize = trimmed
.as_bytes()
.iter()
.position(|&b| {
if b != b'>' {
/* position() is short-circuiting */
true
} else {
quote_depth += 1;
false
}
})
.unwrap_or(0);
trimmed = &trimmed[p_str..];
if trimmed.starts_with(' ') {
/* Remove space stuffing before checking for ending space character.
* [rfc3676#section-4.4] */
trimmed = &trimmed[1..];
}
if trimmed.ends_with(' ') {
if !in_paragraph {
in_paragraph = true;
paragraph_start = prev_index;
} else if prev_quote_depth == quote_depth {
/* This becomes part of the paragraph we're in */
} else {
/*Malformed line, different quote depths can't be in the same paragraph. */
let paragraph_s = &self.text[paragraph_start..prev_index];
reflow_helper2(
&mut paragraph,
paragraph_s,
prev_quote_depth,
in_paragraph,
self.width,
);
paragraph_start = prev_index;
}
} else {
if prev_quote_depth == quote_depth || !in_paragraph {
let paragraph_s = &self.text[paragraph_start..i];
reflow_helper2(
&mut paragraph,
paragraph_s,
quote_depth,
in_paragraph,
self.width,
);
} else {
/*Malformed line, different quote depths can't be in the same paragraph. */
let paragraph_s = &self.text[paragraph_start..prev_index];
reflow_helper2(
&mut paragraph,
paragraph_s,
prev_quote_depth,
in_paragraph,
self.width,
);
let paragraph_s = &self.text[prev_index..i];
reflow_helper2(
&mut paragraph,
paragraph_s,
quote_depth,
false,
self.width,
);
}
*cur_index = i;
std::mem::swap(&mut self.paragraph, &mut paragraph);
paragraph_start = i;
in_paragraph = false;
break;
}
*cur_index = i;
prev_quote_depth = quote_depth;
prev_index = i;
}
if in_paragraph {
let paragraph_s = &self.text[paragraph_start..self.text.len()];
*cur_index = self.text.len();
reflow_helper2(
&mut paragraph,
paragraph_s,
prev_quote_depth,
in_paragraph,
self.width,
);
self.paragraph = paragraph;
}
return self.paragraph.pop_front();
}
ReflowState::ReflowAllWidth {
width,
ref mut state,
} => {
let width = width.saturating_sub(2);
loop {
let line: &str;
let cur_index: &mut usize;
let within_line_index: &mut usize;
let prev_break: &mut usize;
let segment_tree: &segment_tree::SegmentTree;
let breaks: &Vec<(usize, LineBreakCandidate)>;
match state {
LineBreakTextState::AtLine {
cur_index: ref mut _cur_index,
} => {
line = if let Some(line) = self
.text
.get(*_cur_index..)
.and_then(|slice| slice.split('\n').next())
{
line
} else {
*_cur_index = self.text.len();
return None;
};
let _cur_index = *_cur_index;
*state = LineBreakTextState::WithinLine {
line_index: _cur_index,
line_length: line.len(),
within_line_index: 0,
breaks: LineBreakCandidateIter::new(line).collect::<Vec<(
usize,
LineBreakCandidate,
)>>(
),
prev_break: 0,
segment_tree: {
use std::iter::FromIterator;
let mut t: smallvec::SmallVec<[usize; 1024]> =
smallvec::SmallVec::from_iter(
std::iter::repeat(0).take(line.len()),
);
for (idx, _g) in
UnicodeSegmentation::grapheme_indices(line, true)
{
t[idx] = 1;
}
segment_tree::SegmentTree::new(t)
},
};
if let LineBreakTextState::WithinLine {
ref mut line_index,
line_length: _,
within_line_index: ref mut _within_line_index,
breaks: ref _breaks,
prev_break: ref mut _prev_break,
segment_tree: ref _segment_tree,
} = state
{
cur_index = line_index;
within_line_index = _within_line_index;
breaks = _breaks;
prev_break = _prev_break;
segment_tree = _segment_tree;
} else {
unreachable!()
}
}
LineBreakTextState::WithinLine {
ref mut line_index,
ref line_length,
within_line_index: ref mut _within_line_index,
breaks: ref _breaks,
prev_break: ref mut _prev_break,
segment_tree: ref _segment_tree,
} => {
line = &self.text[*line_index..(*line_index + *line_length)];
cur_index = line_index;
within_line_index = _within_line_index;
breaks = _breaks;
prev_break = _prev_break;
segment_tree = _segment_tree;
}
}
if segment_tree.get_sum(0, line.len()) <= width {
*state = LineBreakTextState::AtLine {
cur_index: *cur_index + line.len() + 1,
};
return Some(
line.trim_end_matches(|c| c == '\r' || c == '\n')
.to_string(),
);
}
if breaks.len() < 2 {
let mut line = line;
while !line.is_empty() {
let mut chop_index = std::cmp::min(line.len().saturating_sub(1), width);
while chop_index > 0 && !line.is_char_boundary(chop_index) {
chop_index -= 1;
}
if chop_index == 0 {
self.paragraph.push_back(format!("{}", line));
*cur_index += line.len();
break;
} else {
self.paragraph
.push_back(format!("{}", &line[..chop_index]));
*cur_index += chop_index;
}
line = &line[chop_index..];
}
*state = LineBreakTextState::AtLine {
cur_index: *cur_index,
};
if !self.paragraph.is_empty() {
return self.paragraph.pop_front();
}
continue;
}
while *prev_break < breaks.len() {
let new_off = match breaks[*prev_break..].binary_search_by(|(offset, _)| {
segment_tree
.get_sum(*within_line_index, offset.saturating_sub(1))
.cmp(&width)
}) {
Ok(v) => v,
Err(v) => v,
} + *prev_break;
let end_offset = if new_off >= breaks.len() {
line.len()
} else {
breaks[new_off].0
};
if !line[*within_line_index..end_offset].is_empty() {
if *within_line_index == 0 {
let ret = line[*within_line_index..end_offset]
.trim_end_matches(|c| c == '\r' || c == '\n');
*within_line_index = end_offset;
return Some(ret.to_string());
} else {
let ret = format!(
"⤷{}",
&line[*within_line_index..end_offset]
.trim_end_matches(|c| c == '\r' || c == '\n')
);
*within_line_index = end_offset;
return Some(ret);
}
}
if *within_line_index == end_offset && *prev_break == new_off {
break;
}
*within_line_index = end_offset + 1;
*prev_break = new_off;
}
*state = LineBreakTextState::AtLine {
cur_index: *cur_index + line.len() + 1,
};
}
}
ReflowState::ReflowNo { ref mut cur_index }
| ReflowState::ReflowAll { ref mut cur_index } => {
for line in self.text[*cur_index..].split('\n') {
let ret = line.to_string();
*cur_index += line.len() + 2;
return Some(ret);
}
return None;
}
}
}
}
fn reflow_helper2(
ret: &mut VecDeque<String>,
paragraph: &str,
quote_depth: usize,
in_paragraph: bool,
width: Option<usize>,
) {
if quote_depth > 0 {
let quotes: String = ">".repeat(quote_depth);
let paragraph = paragraph
.trim_start_matches(&quotes)
.replace(&format!("\n{}", &quotes), "")
.replace("\n", "")
.replace("\r", "");
if in_paragraph {
if let Some(width) = width {
ret.extend(
linear(&paragraph, width.saturating_sub(quote_depth))
.into_iter()
.map(|l| format!("{}{}", &quotes, l)),
);
} else {
ret.push_back(format!("{}{}", &quotes, &paragraph));
}
} else {
ret.push_back(format!("{}{}", &quotes, &paragraph));
}
} else {
let paragraph = paragraph.replace("\n", "").replace("\r", "");
if in_paragraph {
if let Some(width) = width {
let ex = linear(&paragraph, width);
ret.extend(ex.into_iter());
} else {
ret.push_back(paragraph);
}
} else {
ret.push_back(paragraph);
}
}
}

View File

@ -33,6 +33,8 @@ pub use wcwidth::*;
pub trait Truncate {
fn truncate_at_boundary(&mut self, new_len: usize);
fn trim_at_boundary(&self, new_len: usize) -> &str;
fn trim_left_at_boundary(&self, new_len: usize) -> &str;
fn truncate_left_at_boundary(&mut self, new_len: usize);
}
impl Truncate for &str {
@ -67,6 +69,39 @@ impl Truncate for &str {
self
}
}
fn trim_left_at_boundary(&self, skip_len: usize) -> &str {
if skip_len >= self.len() {
return "";
}
extern crate unicode_segmentation;
use unicode_segmentation::UnicodeSegmentation;
if let Some((first, _)) = UnicodeSegmentation::grapheme_indices(*self, true)
.skip(skip_len)
.next()
{
&self[first..]
} else {
self
}
}
fn truncate_left_at_boundary(&mut self, skip_len: usize) {
if skip_len >= self.len() {
*self = "";
return;
}
extern crate unicode_segmentation;
use unicode_segmentation::UnicodeSegmentation;
if let Some((first, _)) = UnicodeSegmentation::grapheme_indices(*self, true)
.skip(skip_len)
.next()
{
*self = &self[first..];
}
}
}
impl Truncate for String {
@ -101,6 +136,39 @@ impl Truncate for String {
self.as_str()
}
}
fn trim_left_at_boundary(&self, skip_len: usize) -> &str {
if skip_len >= self.len() {
return "";
}
extern crate unicode_segmentation;
use unicode_segmentation::UnicodeSegmentation;
if let Some((first, _)) = UnicodeSegmentation::grapheme_indices(self.as_str(), true)
.skip(skip_len)
.next()
{
&self[first..]
} else {
self.as_str()
}
}
fn truncate_left_at_boundary(&mut self, skip_len: usize) {
if skip_len >= self.len() {
self.clear();
return;
}
extern crate unicode_segmentation;
use unicode_segmentation::UnicodeSegmentation;
if let Some((first, _)) = UnicodeSegmentation::grapheme_indices(self.as_str(), true)
.skip(skip_len)
.next()
{
*self = self[first..].to_string();
}
}
}
pub trait GlobMatch {

File diff suppressed because it is too large Load Diff

View File

@ -44,7 +44,7 @@ type WChar = u32;
type Interval = (WChar, WChar);
pub struct CodePointsIterator<'a> {
rest: &'a [u8],
rest: std::str::Chars<'a>,
}
/*
@ -61,36 +61,7 @@ impl<'a> Iterator for CodePointsIterator<'a> {
type Item = WChar;
fn next(&mut self) -> Option<WChar> {
if self.rest.is_empty() {
return None;
}
/* Input is UTF-8 valid strings, guaranteed by Rust's std */
if self.rest[0] & 0b1000_0000 == 0x0 {
let ret: WChar = WChar::from(self.rest[0]);
self.rest = &self.rest[1..];
return Some(ret);
}
if self.rest[0] & 0b1110_0000 == 0b1100_0000 {
let ret: WChar = (WChar::from(self.rest[0]) & 0b0001_1111).rotate_left(6)
+ (WChar::from(self.rest[1]) & 0b0111_1111);
self.rest = &self.rest[2..];
return Some(ret);
}
if self.rest[0] & 0b1111_0000 == 0b1110_0000 {
let ret: WChar = (WChar::from(self.rest[0]) & 0b0000_0111).rotate_left(12)
+ (WChar::from(self.rest[1]) & 0b0011_1111).rotate_left(6)
+ (WChar::from(self.rest[2]) & 0b0011_1111);
self.rest = &self.rest[3..];
return Some(ret);
}
let ret: WChar = (WChar::from(self.rest[0]) & 0b0000_0111).rotate_left(18)
+ (WChar::from(self.rest[1]) & 0b0011_1111).rotate_left(12)
+ (WChar::from(self.rest[2]) & 0b0011_1111).rotate_left(6)
+ (WChar::from(self.rest[3]) & 0b0011_1111);
self.rest = &self.rest[4..];
Some(ret)
self.rest.next().map(|c| c as WChar)
}
}
pub trait CodePointsIter {
@ -99,16 +70,12 @@ pub trait CodePointsIter {
impl CodePointsIter for str {
fn code_points(&self) -> CodePointsIterator {
CodePointsIterator {
rest: self.as_bytes(),
}
CodePointsIterator { rest: self.chars() }
}
}
impl CodePointsIter for &str {
fn code_points(&self) -> CodePointsIterator {
CodePointsIterator {
rest: self.as_bytes(),
}
CodePointsIterator { rest: self.chars() }
}
}
@ -136,174 +103,54 @@ fn bisearch(ucs: WChar, table: &'static [Interval]) -> bool {
false
}
/* The following functions define the column width of an ISO 10646
* character as follows:
*
* - The null character (U+0000) has a column width of 0.
*
* - Other C0/C1 control characters and DEL will lead to a return
* value of -1.
*
* - Non-spacing and enclosing combining characters (general
* category code Mn or Me in the Unicode database) have a
* column width of 0.
*
* - Other format characters (general category code Cf in the Unicode
* database) and ZERO WIDTH SPACE (U+200B) have a column width of 0.
*
* - Hangul Jamo medial vowels and final consonants (U+1160-U+11FF)
* have a column width of 0.
*
* - Spacing characters in the East Asian Wide (W) or East Asian
* FullWidth (F) category as defined in Unicode Technical
* Report #11 have a column width of 2.
*
* - All remaining characters (including all printable
* ISO 8859-1 and WGL4 characters, Unicode control characters,
* etc.) have a column width of 1.
*
* This implementation assumes that wchar_t characters are encoded
* in ISO 10646.
*/
pub fn wcwidth(ucs: WChar) -> Option<usize> {
/* sorted list of non-overlapping intervals of non-spacing characters */
const COMBINING: &[Interval] = &[
(0x0300, 0x034E),
(0x0360, 0x0362),
(0x0483, 0x0486),
(0x0488, 0x0489),
(0x0591, 0x05A1),
(0x05A3, 0x05B9),
(0x05BB, 0x05BD),
(0x05BF, 0x05BF),
(0x05C1, 0x05C2),
(0x05C4, 0x05C4),
(0x064B, 0x0655),
(0x0670, 0x0670),
(0x06D6, 0x06E4),
(0x06E7, 0x06E8),
(0x06EA, 0x06ED),
(0x070F, 0x070F),
(0x0711, 0x0711),
(0x0730, 0x074A),
(0x07A6, 0x07B0),
(0x0901, 0x0902),
(0x093C, 0x093C),
(0x0941, 0x0948),
(0x094D, 0x094D),
(0x0951, 0x0954),
(0x0962, 0x0963),
(0x0981, 0x0981),
(0x09BC, 0x09BC),
(0x09C1, 0x09C4),
(0x09CD, 0x09CD),
(0x09E2, 0x09E3),
(0x0A02, 0x0A02),
(0x0A3C, 0x0A3C),
(0x0A41, 0x0A42),
(0x0A47, 0x0A48),
(0x0A4B, 0x0A4D),
(0x0A70, 0x0A71),
(0x0A81, 0x0A82),
(0x0ABC, 0x0ABC),
(0x0AC1, 0x0AC5),
(0x0AC7, 0x0AC8),
(0x0ACD, 0x0ACD),
(0x0B01, 0x0B01),
(0x0B3C, 0x0B3C),
(0x0B3F, 0x0B3F),
(0x0B41, 0x0B43),
(0x0B4D, 0x0B4D),
(0x0B56, 0x0B56),
(0x0B82, 0x0B82),
(0x0BC0, 0x0BC0),
(0x0BCD, 0x0BCD),
(0x0C3E, 0x0C40),
(0x0C46, 0x0C48),
(0x0C4A, 0x0C4D),
(0x0C55, 0x0C56),
(0x0CBF, 0x0CBF),
(0x0CC6, 0x0CC6),
(0x0CCC, 0x0CCD),
(0x0D41, 0x0D43),
(0x0D4D, 0x0D4D),
(0x0DCA, 0x0DCA),
(0x0DD2, 0x0DD4),
(0x0DD6, 0x0DD6),
(0x0E31, 0x0E31),
(0x0E34, 0x0E3A),
(0x0E47, 0x0E4E),
(0x0EB1, 0x0EB1),
(0x0EB4, 0x0EB9),
(0x0EBB, 0x0EBC),
(0x0EC8, 0x0ECD),
(0x0F18, 0x0F19),
(0x0F35, 0x0F35),
(0x0F37, 0x0F37),
(0x0F39, 0x0F39),
(0x0F71, 0x0F7E),
(0x0F80, 0x0F84),
(0x0F86, 0x0F87),
(0x0F90, 0x0F97),
(0x0F99, 0x0FBC),
(0x0FC6, 0x0FC6),
(0x102D, 0x1030),
(0x1032, 0x1032),
(0x1036, 0x1037),
(0x1039, 0x1039),
(0x1058, 0x1059),
(0x1160, 0x11FF),
(0x17B7, 0x17BD),
(0x17C6, 0x17C6),
(0x17C9, 0x17D3),
(0x180B, 0x180E),
(0x18A9, 0x18A9),
(0x200B, 0x200F),
(0x202A, 0x202E),
(0x206A, 0x206F),
(0x20D0, 0x20E3),
(0x302A, 0x302F),
(0x3099, 0x309A),
(0xFB1E, 0xFB1E),
(0xFE20, 0xFE23),
(0xFEFF, 0xFEFF),
(0xFFF9, 0xFFFB),
];
/* test for 8-bit control characters */
if ucs == 0 {
return Some(0);
}
if ucs < 32 || (ucs >= 0x7f && ucs < 0xa0) {
return None;
if bisearch(ucs, super::tables::ASCII) {
Some(1)
} else if bisearch(ucs, super::tables::PRIVATE) {
None
} else if bisearch(ucs, super::tables::NONPRINT) {
None
} else if bisearch(ucs, super::tables::COMBINING) {
None
} else if bisearch(ucs, super::tables::DOUBLEWIDE) {
Some(2)
} else if bisearch(ucs, super::tables::AMBIGUOUS) {
Some(1)
} else if bisearch(ucs, super::tables::UNASSIGNED) {
Some(2)
} else if bisearch(ucs, super::tables::WIDENEDIN9) {
Some(2)
} else {
Some(1)
}
}
/* binary search in table of emojis */
if bisearch(ucs, EMOJI_RANGES) {
return Some(2);
}
/* binary search in table of non-spacing characters */
if bisearch(ucs, COMBINING) {
return Some(1);
}
/* if we arrive here, ucs is not a combining or C0/C1 control character */
Some(
1 + big_if_true!(
ucs >= 0x1100
&& (ucs <= 0x115f || /* Hangul Jamo init. consonants */
(ucs >= 0x2e80 && ucs <= 0xa4cf && (ucs & !0x0011) != 0x300a &&
ucs != 0x303f) || /* CJK ... Yi */
(ucs >= 0xac00 && ucs <= 0xd7a3) || /* Hangul Syllables */
(ucs >= 0xf900 && ucs <= 0xfaff) || /* CJK Compatibility Ideographs */
(ucs >= 0xfe30 && ucs <= 0xfe6f) || /* CJK Compatibility Forms */
(ucs >= 0xff00 && ucs <= 0xff5f) || /* Fullwidth Forms */
(ucs >= 0xffe0 && ucs <= 0xffe6) ||
(ucs >= 0x20000 && ucs <= 0x2ffff))
),
)
#[test]
fn test_wcwidth() {
assert_eq!(
&"abc\0".code_points().collect::<Vec<_>>(),
&[0x61, 0x62, 0x63, 0x0]
);
assert_eq!(&"".code_points().collect::<Vec<_>>(), &[0x25cf]);
assert_eq!(&"📎".code_points().collect::<Vec<_>>(), &[0x1f4ce]);
assert_eq!(
&"𐼹𐼺𐼻𐼼𐼽".code_points().collect::<Vec<_>>(),
&[0x10F39, 0x10F3A, 0x10F3B, 0x10F3C, 0x10F3D]
); // Sogdian alphabet
assert_eq!(
&"𐼹a𐼽b".code_points().collect::<Vec<_>>(),
&[0x10F39, 0x61, 0x10F3D, 0x62]
); // Sogdian alphabet
assert_eq!(
&"📎\u{FE0E}".code_points().collect::<Vec<_>>(),
&[0x1f4ce, 0xfe0e]
);
use crate::text_processing::grapheme_clusters::TextProcessing;
assert_eq!("".grapheme_width(), 1);
assert_eq!("●📎".grapheme_width(), 3);
assert_eq!("\u{FE0E}📎\u{FE0E}".grapheme_width(), 3);
assert_eq!("🎃".grapheme_width(), 2);
assert_eq!("👻".grapheme_width(), 2);
}
pub fn wcswidth(mut pwcs: WChar, mut n: usize) -> Option<usize> {
@ -322,360 +169,3 @@ pub fn wcswidth(mut pwcs: WChar, mut n: usize) -> Option<usize> {
Some(width)
}
const EMOJI_RANGES: &[Interval] = &[
(0x231A, 0x231B), // ; Basic_Emoji ; watch # 1.1 [2] (⌚..⌛)
(0x23E9, 0x23EC), // ; Basic_Emoji ; fast-forward button # 6.0 [4] (⏩..⏬)
(0x23F0, 0x23F0), // ; Basic_Emoji ; alarm clock # 6.0 [1] (⏰)
(0x23F3, 0x23F3), // ; Basic_Emoji ; hourglass not done # 6.0 [1] (⏳)
(0x25FD, 0x25FE), // ; Basic_Emoji ; white medium-small square # 3.2 [2] (◽..◾)
(0x2614, 0x2615), // ; Basic_Emoji ; umbrella with rain drops # 4.0 [2] (☔..☕)
(0x2648, 0x2653), // ; Basic_Emoji ; Aries # 1.1 [12] (♈..♓)
(0x267F, 0x267F), // ; Basic_Emoji ; wheelchair symbol # 4.1 [1] (♿)
(0x2693, 0x2693), // ; Basic_Emoji ; anchor # 4.1 [1] (⚓)
(0x26A1, 0x26A1), // ; Basic_Emoji ; high voltage # 4.0 [1] (⚡)
(0x26AA, 0x26AB), // ; Basic_Emoji ; white circle # 4.1 [2] (⚪..⚫)
(0x26BD, 0x26BE), // ; Basic_Emoji ; soccer ball # 5.2 [2] (⚽..⚾)
(0x26C4, 0x26C5), // ; Basic_Emoji ; snowman without snow # 5.2 [2] (⛄..⛅)
(0x26CE, 0x26CE), // ; Basic_Emoji ; Ophiuchus # 6.0 [1] (⛎)
(0x26D4, 0x26D4), // ; Basic_Emoji ; no entry # 5.2 [1] (⛔)
(0x26EA, 0x26EA), // ; Basic_Emoji ; church # 5.2 [1] (⛪)
(0x26F2, 0x26F3), // ; Basic_Emoji ; fountain # 5.2 [2] (⛲..⛳)
(0x26F5, 0x26F5), // ; Basic_Emoji ; sailboat # 5.2 [1] (⛵)
(0x26FA, 0x26FA), // ; Basic_Emoji ; tent # 5.2 [1] (⛺)
(0x26FD, 0x26FD), // ; Basic_Emoji ; fuel pump # 5.2 [1] (⛽)
(0x2705, 0x2705), // ; Basic_Emoji ; check mark button # 6.0 [1] (✅)
(0x270A, 0x270B), // ; Basic_Emoji ; raised fist # 6.0 [2] (✊..✋)
(0x2728, 0x2728), // ; Basic_Emoji ; sparkles # 6.0 [1] (✨)
(0x274C, 0x274C), // ; Basic_Emoji ; cross mark # 6.0 [1] (❌)
(0x274E, 0x274E), // ; Basic_Emoji ; cross mark button # 6.0 [1] (❎)
(0x2753, 0x2755), // ; Basic_Emoji ; question mark # 6.0 [3] (❓..❕)
(0x2757, 0x2757), // ; Basic_Emoji ; exclamation mark # 5.2 [1] (❗)
(0x2795, 0x2797), // ; Basic_Emoji ; plus sign # 6.0 [3] (..➗)
(0x27B0, 0x27B0), // ; Basic_Emoji ; curly loop # 6.0 [1] (➰)
(0x27BF, 0x27BF), // ; Basic_Emoji ; double curly loop # 6.0 [1] (➿)
(0x2B1B, 0x2B1C), // ; Basic_Emoji ; black large square # 5.1 [2] (⬛..⬜)
(0x2B50, 0x2B50), // ; Basic_Emoji ; star # 5.1 [1] (⭐)
(0x2B55, 0x2B55), // ; Basic_Emoji ; hollow red circle # 5.2 [1] (⭕)
(0x1F004, 0x1F004), // ; Basic_Emoji ; mahjong red dragon # 5.1 [1] (🀄)
(0x1F0CF, 0x1F0CF), // ; Basic_Emoji ; joker # 6.0 [1] (🃏)
(0x1F18E, 0x1F18E), // ; Basic_Emoji ; AB button (blood type) # 6.0 [1] (🆎)
(0x1F191, 0x1F19A), // ; Basic_Emoji ; CL button # 6.0 [10] (🆑..🆚)
(0x1F201, 0x1F201), // ; Basic_Emoji ; Japanese “here” button # 6.0 [1] (🈁)
(0x1F21A, 0x1F21A), // ; Basic_Emoji ; Japanese “free of charge” button # 5.2 [1] (🈚)
(0x1F22F, 0x1F22F), // ; Basic_Emoji ; Japanese “reserved” button # 5.2 [1] (🈯)
(0x1F232, 0x1F236), // ; Basic_Emoji ; Japanese “prohibited” button # 6.0 [5] (🈲..🈶)
(0x1F238, 0x1F23A), // ; Basic_Emoji ; Japanese “application” button # 6.0 [3] (🈸..🈺)
(0x1F250, 0x1F251), // ; Basic_Emoji ; Japanese “bargain” button # 6.0 [2] (🉐..🉑)
(0x1F300, 0x1F320), // ; Basic_Emoji ; cyclone # 6.0 [33] (🌀..🌠)
(0x1F32D, 0x1F32F), // ; Basic_Emoji ; hot dog # 8.0 [3] (🌭..🌯)
(0x1F330, 0x1F335), // ; Basic_Emoji ; chestnut # 6.0 [6] (🌰..🌵)
(0x1F337, 0x1F37C), // ; Basic_Emoji ; tulip # 6.0 [70] (🌷..🍼)
(0x1F37E, 0x1F37F), // ; Basic_Emoji ; bottle with popping cork # 8.0 [2] (🍾..🍿)
(0x1F380, 0x1F393), // ; Basic_Emoji ; ribbon # 6.0 [20] (🎀..🎓)
(0x1F3A0, 0x1F3C4), // ; Basic_Emoji ; carousel horse # 6.0 [37] (🎠..🏄)
(0x1F3C5, 0x1F3C5), // ; Basic_Emoji ; sports medal # 7.0 [1] (🏅)
(0x1F3C6, 0x1F3CA), // ; Basic_Emoji ; trophy # 6.0 [5] (🏆..🏊)
(0x1F3CF, 0x1F3D3), // ; Basic_Emoji ; cricket game # 8.0 [5] (🏏..🏓)
(0x1F3E0, 0x1F3F0), // ; Basic_Emoji ; house # 6.0 [17] (🏠..🏰)
(0x1F3F4, 0x1F3F4), // ; Basic_Emoji ; black flag # 7.0 [1] (🏴)
(0x1F3F8, 0x1F3FF), // ; Basic_Emoji ; badminton # 8.0 [8] (🏸..🏿)
(0x1F400, 0x1F43E), // ; Basic_Emoji ; rat # 6.0 [63] (🐀..🐾)
(0x1F440, 0x1F440), // ; Basic_Emoji ; eyes # 6.0 [1] (👀)
(0x1F442, 0x1F4F7), // ; Basic_Emoji ; ear # 6.0[182] (👂..📷)
(0x1F4F8, 0x1F4F8), // ; Basic_Emoji ; camera with flash # 7.0 [1] (📸)
(0x1F4F9, 0x1F4FC), // ; Basic_Emoji ; video camera # 6.0 [4] (📹..📼)
(0x1F4FF, 0x1F4FF), // ; Basic_Emoji ; prayer beads # 8.0 [1] (📿)
(0x1F500, 0x1F53D), // ; Basic_Emoji ; shuffle tracks button # 6.0 [62] (🔀..🔽)
(0x1F54B, 0x1F54E), // ; Basic_Emoji ; kaaba # 8.0 [4] (🕋..🕎)
(0x1F550, 0x1F567), // ; Basic_Emoji ; one oclock # 6.0 [24] (🕐..🕧)
(0x1F57A, 0x1F57A), // ; Basic_Emoji ; man dancing # 9.0 [1] (🕺)
(0x1F595, 0x1F596), // ; Basic_Emoji ; middle finger # 7.0 [2] (🖕..🖖)
(0x1F5A4, 0x1F5A4), // ; Basic_Emoji ; black heart # 9.0 [1] (🖤)
(0x1F5FB, 0x1F5FF), // ; Basic_Emoji ; mount fuji # 6.0 [5] (🗻..🗿)
(0x1F600, 0x1F600), // ; Basic_Emoji ; grinning face # 6.1 [1] (😀)
(0x1F601, 0x1F610), // ; Basic_Emoji ; beaming face with smiling eyes # 6.0 [16] (😁..😐)
(0x1F611, 0x1F611), // ; Basic_Emoji ; expressionless face # 6.1 [1] (😑)
(0x1F612, 0x1F614), // ; Basic_Emoji ; unamused face # 6.0 [3] (😒..😔)
(0x1F615, 0x1F615), // ; Basic_Emoji ; confused face # 6.1 [1] (😕)
(0x1F616, 0x1F616), // ; Basic_Emoji ; confounded face # 6.0 [1] (😖)
(0x1F617, 0x1F617), // ; Basic_Emoji ; kissing face # 6.1 [1] (😗)
(0x1F618, 0x1F618), // ; Basic_Emoji ; face blowing a kiss # 6.0 [1] (😘)
(0x1F619, 0x1F619), // ; Basic_Emoji ; kissing face with smiling eyes # 6.1 [1] (😙)
(0x1F61A, 0x1F61A), // ; Basic_Emoji ; kissing face with closed eyes # 6.0 [1] (😚)
(0x1F61B, 0x1F61B), // ; Basic_Emoji ; face with tongue # 6.1 [1] (😛)
(0x1F61C, 0x1F61E), // ; Basic_Emoji ; winking face with tongue # 6.0 [3] (😜..😞)
(0x1F61F, 0x1F61F), // ; Basic_Emoji ; worried face # 6.1 [1] (😟)
(0x1F620, 0x1F625), // ; Basic_Emoji ; angry face # 6.0 [6] (😠..😥)
(0x1F626, 0x1F627), // ; Basic_Emoji ; frowning face with open mouth # 6.1 [2] (😦..😧)
(0x1F628, 0x1F62B), // ; Basic_Emoji ; fearful face # 6.0 [4] (😨..😫)
(0x1F62C, 0x1F62C), // ; Basic_Emoji ; grimacing face # 6.1 [1] (😬)
(0x1F62D, 0x1F62D), // ; Basic_Emoji ; loudly crying face # 6.0 [1] (😭)
(0x1F62E, 0x1F62F), // ; Basic_Emoji ; face with open mouth # 6.1 [2] (😮..😯)
(0x1F630, 0x1F633), // ; Basic_Emoji ; anxious face with sweat # 6.0 [4] (😰..😳)
(0x1F634, 0x1F634), // ; Basic_Emoji ; sleeping face # 6.1 [1] (😴)
(0x1F635, 0x1F640), // ; Basic_Emoji ; dizzy face # 6.0 [12] (😵..🙀)
(0x1F641, 0x1F642), // ; Basic_Emoji ; slightly frowning face # 7.0 [2] (🙁..🙂)
(0x1F643, 0x1F644), // ; Basic_Emoji ; upside-down face # 8.0 [2] (🙃..🙄)
(0x1F645, 0x1F64F), // ; Basic_Emoji ; person gesturing NO # 6.0 [11] (🙅..🙏)
(0x1F680, 0x1F6C5), // ; Basic_Emoji ; rocket # 6.0 [70] (🚀..🛅)
(0x1F6CC, 0x1F6CC), // ; Basic_Emoji ; person in bed # 7.0 [1] (🛌)
(0x1F6D0, 0x1F6D0), // ; Basic_Emoji ; place of worship # 8.0 [1] (🛐)
(0x1F6D1, 0x1F6D2), // ; Basic_Emoji ; stop sign # 9.0 [2] (🛑..🛒)
(0x1F6D5, 0x1F6D5), // ; Basic_Emoji ; hindu temple # 12.0 [1] (🛕)
(0x1F6EB, 0x1F6EC), // ; Basic_Emoji ; airplane departure # 7.0 [2] (🛫..🛬)
(0x1F6F4, 0x1F6F6), // ; Basic_Emoji ; kick scooter # 9.0 [3] (🛴..🛶)
(0x1F6F7, 0x1F6F8), // ; Basic_Emoji ; sled # 10.0 [2] (🛷..🛸)
(0x1F6F9, 0x1F6F9), // ; Basic_Emoji ; skateboard # 11.0 [1] (🛹)
(0x1F6FA, 0x1F6FA), // ; Basic_Emoji ; auto rickshaw # 12.0 [1] (🛺)
(0x1F7E0, 0x1F7EB), // ; Basic_Emoji ; orange circle # 12.0 [12] (🟠..🟫)
(0x1F90D, 0x1F90F), // ; Basic_Emoji ; white heart # 12.0 [3] (🤍..🤏)
(0x1F910, 0x1F918), // ; Basic_Emoji ; zipper-mouth face # 8.0 [9] (🤐..🤘)
(0x1F919, 0x1F91E), // ; Basic_Emoji ; call me hand # 9.0 [6] (🤙..🤞)
(0x1F91F, 0x1F91F), // ; Basic_Emoji ; love-you gesture # 10.0 [1] (🤟)
(0x1F920, 0x1F927), // ; Basic_Emoji ; cowboy hat face # 9.0 [8] (🤠..🤧)
(0x1F928, 0x1F92F), // ; Basic_Emoji ; face with raised eyebrow # 10.0 [8] (🤨..🤯)
(0x1F930, 0x1F930), // ; Basic_Emoji ; pregnant woman # 9.0 [1] (🤰)
(0x1F931, 0x1F932), // ; Basic_Emoji ; breast-feeding # 10.0 [2] (🤱..🤲)
(0x1F933, 0x1F93A), // ; Basic_Emoji ; selfie # 9.0 [8] (🤳..🤺)
(0x1F93C, 0x1F93E), // ; Basic_Emoji ; people wrestling # 9.0 [3] (🤼..🤾)
(0x1F93F, 0x1F93F), // ; Basic_Emoji ; diving mask # 12.0 [1] (🤿)
(0x1F940, 0x1F945), // ; Basic_Emoji ; wilted flower # 9.0 [6] (🥀..🥅)
(0x1F947, 0x1F94B), // ; Basic_Emoji ; 1st place medal # 9.0 [5] (🥇..🥋)
(0x1F94C, 0x1F94C), // ; Basic_Emoji ; curling stone # 10.0 [1] (🥌)
(0x1F94D, 0x1F94F), // ; Basic_Emoji ; lacrosse # 11.0 [3] (🥍..🥏)
(0x1F950, 0x1F95E), // ; Basic_Emoji ; croissant # 9.0 [15] (🥐..🥞)
(0x1F95F, 0x1F96B), // ; Basic_Emoji ; dumpling # 10.0 [13] (🥟..🥫)
(0x1F96C, 0x1F970), // ; Basic_Emoji ; leafy green # 11.0 [5] (🥬..🥰)
(0x1F971, 0x1F971), // ; Basic_Emoji ; yawning face # 12.0 [1] (🥱)
(0x1F973, 0x1F976), // ; Basic_Emoji ; partying face # 11.0 [4] (🥳..🥶)
(0x1F97A, 0x1F97A), // ; Basic_Emoji ; pleading face # 11.0 [1] (🥺)
(0x1F97B, 0x1F97B), // ; Basic_Emoji ; sari # 12.0 [1] (🥻)
(0x1F97C, 0x1F97F), // ; Basic_Emoji ; lab coat # 11.0 [4] (🥼..🥿)
(0x1F980, 0x1F984), // ; Basic_Emoji ; crab # 8.0 [5] (🦀..🦄)
(0x1F985, 0x1F991), // ; Basic_Emoji ; eagle # 9.0 [13] (🦅..🦑)
(0x1F992, 0x1F997), // ; Basic_Emoji ; giraffe # 10.0 [6] (🦒..🦗)
(0x1F998, 0x1F9A2), // ; Basic_Emoji ; kangaroo # 11.0 [11] (🦘..🦢)
(0x1F9A5, 0x1F9AA), // ; Basic_Emoji ; sloth # 12.0 [6] (🦥..🦪)
(0x1F9AE, 0x1F9AF), // ; Basic_Emoji ; guide dog # 12.0 [2] (🦮..🦯)
(0x1F9B0, 0x1F9B9), // ; Basic_Emoji ; red hair # 11.0 [10] (🦰..🦹)
(0x1F9BA, 0x1F9BF), // ; Basic_Emoji ; safety vest # 12.0 [6] (🦺..🦿)
(0x1F9C0, 0x1F9C0), // ; Basic_Emoji ; cheese wedge # 8.0 [1] (🧀)
(0x1F9C1, 0x1F9C2), // ; Basic_Emoji ; cupcake # 11.0 [2] (🧁..🧂)
(0x1F9C3, 0x1F9CA), // ; Basic_Emoji ; beverage box # 12.0 [8] (🧃..🧊)
(0x1F9CD, 0x1F9CF), // ; Basic_Emoji ; person standing # 12.0 [3] (🧍..🧏)
(0x1F9D0, 0x1F9E6), // ; Basic_Emoji ; face with monocle # 10.0 [23] (🧐..🧦)
(0x1F9E7, 0x1F9FF), // ; Basic_Emoji ; red envelope # 11.0 [25] (🧧..🧿)
(0x1FA70, 0x1FA73), // ; Basic_Emoji ; ballet shoes # 12.0 [4] (🩰..🩳)
(0x1FA78, 0x1FA7A), // ; Basic_Emoji ; drop of blood # 12.0 [3] (🩸..🩺)
(0x1FA80, 0x1FA82), // ; Basic_Emoji ; yo-yo # 12.0 [3] (🪀..🪂)
(0x1FA90, 0x1FA95), // ; Basic_Emoji ; ringed planet # 12.0 [6] (🪐..🪕)
];
/*
00A9 FE0F ; Basic_Emoji ; copyright # 3.2 [1] (©)
00AE FE0F ; Basic_Emoji ; registered # 3.2 [1] (®)
203C FE0F ; Basic_Emoji ; double exclamation mark # 3.2 [1] ()
2049 FE0F ; Basic_Emoji ; exclamation question mark # 3.2 [1] ()
2122 FE0F ; Basic_Emoji ; trade mark # 3.2 [1] ()
2139 FE0F ; Basic_Emoji ; information # 3.2 [1] ()
2194 FE0F ; Basic_Emoji ; left-right arrow # 3.2 [1] ()
2195 FE0F ; Basic_Emoji ; up-down arrow # 3.2 [1] ()
2196 FE0F ; Basic_Emoji ; up-left arrow # 3.2 [1] ()
2197 FE0F ; Basic_Emoji ; up-right arrow # 3.2 [1] ()
2198 FE0F ; Basic_Emoji ; down-right arrow # 3.2 [1] ()
2199 FE0F ; Basic_Emoji ; down-left arrow # 3.2 [1] ()
21A9 FE0F ; Basic_Emoji ; right arrow curving left # 3.2 [1] ()
21AA FE0F ; Basic_Emoji ; left arrow curving right # 3.2 [1] ()
2328 FE0F ; Basic_Emoji ; keyboard # 3.2 [1] ()
23CF FE0F ; Basic_Emoji ; eject button # 4.0 [1] ()
23ED FE0F ; Basic_Emoji ; next track button # 6.0 [1] ()
23EE FE0F ; Basic_Emoji ; last track button # 6.0 [1] ()
23EF FE0F ; Basic_Emoji ; play or pause button # 6.0 [1] ()
23F1 FE0F ; Basic_Emoji ; stopwatch # 6.0 [1] ()
23F2 FE0F ; Basic_Emoji ; timer clock # 6.0 [1] ()
23F8 FE0F ; Basic_Emoji ; pause button # 7.0 [1] ()
23F9 FE0F ; Basic_Emoji ; stop button # 7.0 [1] ()
23FA FE0F ; Basic_Emoji ; record button # 7.0 [1] ()
24C2 FE0F ; Basic_Emoji ; circled M # 3.2 [1] ()
25AA FE0F ; Basic_Emoji ; black small square # 3.2 [1] ()
25AB FE0F ; Basic_Emoji ; white small square # 3.2 [1] ()
25B6 FE0F ; Basic_Emoji ; play button # 3.2 [1] ()
25C0 FE0F ; Basic_Emoji ; reverse button # 3.2 [1] ()
25FB FE0F ; Basic_Emoji ; white medium square # 3.2 [1] ()
25FC FE0F ; Basic_Emoji ; black medium square # 3.2 [1] ()
2600 FE0F ; Basic_Emoji ; sun # 3.2 [1] ()
2601 FE0F ; Basic_Emoji ; cloud # 3.2 [1] ()
2602 FE0F ; Basic_Emoji ; umbrella # 3.2 [1] ()
2603 FE0F ; Basic_Emoji ; snowman # 3.2 [1] ()
2604 FE0F ; Basic_Emoji ; comet # 3.2 [1] ()
260E FE0F ; Basic_Emoji ; telephone # 3.2 [1] ()
2611 FE0F ; Basic_Emoji ; check box with check # 3.2 [1] ()
2618 FE0F ; Basic_Emoji ; shamrock # 4.1 [1] ()
261D FE0F ; Basic_Emoji ; index pointing up # 3.2 [1] ()
2620 FE0F ; Basic_Emoji ; skull and crossbones # 3.2 [1] ()
2622 FE0F ; Basic_Emoji ; radioactive # 3.2 [1] ()
2623 FE0F ; Basic_Emoji ; biohazard # 3.2 [1] ()
2626 FE0F ; Basic_Emoji ; orthodox cross # 3.2 [1] ()
262A FE0F ; Basic_Emoji ; star and crescent # 3.2 [1] ()
262E FE0F ; Basic_Emoji ; peace symbol # 3.2 [1] ()
262F FE0F ; Basic_Emoji ; yin yang # 3.2 [1] ()
2638 FE0F ; Basic_Emoji ; wheel of dharma # 3.2 [1] ()
2639 FE0F ; Basic_Emoji ; frowning face # 3.2 [1] ()
263A FE0F ; Basic_Emoji ; smiling face # 3.2 [1] ()
2640 FE0F ; Basic_Emoji ; female sign # 3.2 [1] ()
2642 FE0F ; Basic_Emoji ; male sign # 3.2 [1] ()
265F FE0F ; Basic_Emoji ; chess pawn # 3.2 [1] ()
2660 FE0F ; Basic_Emoji ; spade suit # 3.2 [1] ()
2663 FE0F ; Basic_Emoji ; club suit # 3.2 [1] ()
2665 FE0F ; Basic_Emoji ; heart suit # 3.2 [1] ()
2666 FE0F ; Basic_Emoji ; diamond suit # 3.2 [1] ()
2668 FE0F ; Basic_Emoji ; hot springs # 3.2 [1] ()
267B FE0F ; Basic_Emoji ; recycling symbol # 3.2 [1] ()
267E FE0F ; Basic_Emoji ; infinity # 4.1 [1] ()
2692 FE0F ; Basic_Emoji ; hammer and pick # 4.1 [1] ()
2694 FE0F ; Basic_Emoji ; crossed swords # 4.1 [1] ()
2695 FE0F ; Basic_Emoji ; medical symbol # 4.1 [1] ()
2696 FE0F ; Basic_Emoji ; balance scale # 4.1 [1] ()
2697 FE0F ; Basic_Emoji ; alembic # 4.1 [1] ()
2699 FE0F ; Basic_Emoji ; gear # 4.1 [1] ()
269B FE0F ; Basic_Emoji ; atom symbol # 4.1 [1] ()
269C FE0F ; Basic_Emoji ; fleur-de-lis # 4.1 [1] ()
26A0 FE0F ; Basic_Emoji ; warning # 4.0 [1] ()
26B0 FE0F ; Basic_Emoji ; coffin # 4.1 [1] ()
26B1 FE0F ; Basic_Emoji ; funeral urn # 4.1 [1] ()
26C8 FE0F ; Basic_Emoji ; cloud with lightning and rain # 5.2 [1] ()
26CF FE0F ; Basic_Emoji ; pick # 5.2 [1] ()
26D1 FE0F ; Basic_Emoji ; rescue workers helmet # 5.2 [1] ()
26D3 FE0F ; Basic_Emoji ; chains # 5.2 [1] ()
26E9 FE0F ; Basic_Emoji ; shinto shrine # 5.2 [1] ()
26F0 FE0F ; Basic_Emoji ; mountain # 5.2 [1] ()
26F1 FE0F ; Basic_Emoji ; umbrella on ground # 5.2 [1] ()
26F4 FE0F ; Basic_Emoji ; ferry # 5.2 [1] ()
26F7 FE0F ; Basic_Emoji ; skier # 5.2 [1] ()
26F8 FE0F ; Basic_Emoji ; ice skate # 5.2 [1] ()
26F9 FE0F ; Basic_Emoji ; person bouncing ball # 5.2 [1] ()
2702 FE0F ; Basic_Emoji ; scissors # 3.2 [1] ()
2708 FE0F ; Basic_Emoji ; airplane # 3.2 [1] ()
2709 FE0F ; Basic_Emoji ; envelope # 3.2 [1] ()
270C FE0F ; Basic_Emoji ; victory hand # 3.2 [1] ()
270D FE0F ; Basic_Emoji ; writing hand # 3.2 [1] ()
270F FE0F ; Basic_Emoji ; pencil # 3.2 [1] ()
2712 FE0F ; Basic_Emoji ; black nib # 3.2 [1] ()
2714 FE0F ; Basic_Emoji ; check mark # 3.2 [1] ()
2716 FE0F ; Basic_Emoji ; multiplication sign # 3.2 [1] ()
271D FE0F ; Basic_Emoji ; latin cross # 3.2 [1] ()
2721 FE0F ; Basic_Emoji ; star of David # 3.2 [1] ()
2733 FE0F ; Basic_Emoji ; eight-spoked asterisk # 3.2 [1] ()
2734 FE0F ; Basic_Emoji ; eight-pointed star # 3.2 [1] ()
2744 FE0F ; Basic_Emoji ; snowflake # 3.2 [1] ()
2747 FE0F ; Basic_Emoji ; sparkle # 3.2 [1] ()
2763 FE0F ; Basic_Emoji ; heart exclamation # 3.2 [1] ()
2764 FE0F ; Basic_Emoji ; red heart # 3.2 [1] ()
27A1 FE0F ; Basic_Emoji ; right arrow # 3.2 [1] ()
2934 FE0F ; Basic_Emoji ; right arrow curving up # 3.2 [1] ()
2935 FE0F ; Basic_Emoji ; right arrow curving down # 3.2 [1] ()
2B05 FE0F ; Basic_Emoji ; left arrow # 4.0 [1] ()
2B06 FE0F ; Basic_Emoji ; up arrow # 4.0 [1] ()
2B07 FE0F ; Basic_Emoji ; down arrow # 4.0 [1] ()
3030 FE0F ; Basic_Emoji ; wavy dash # 3.2 [1] ()
303D FE0F ; Basic_Emoji ; part alternation mark # 3.2 [1] ()
3297 FE0F ; Basic_Emoji ; Japanese congratulations button # 3.2 [1] ()
3299 FE0F ; Basic_Emoji ; Japanese secret button # 3.2 [1] ()
1F170 FE0F ; Basic_Emoji ; A button (blood type) # 6.0 [1] (🅰)
1F171 FE0F ; Basic_Emoji ; B button (blood type) # 6.0 [1] (🅱)
1F17E FE0F ; Basic_Emoji ; O button (blood type) # 6.0 [1] (🅾)
1F17F FE0F ; Basic_Emoji ; P button # 5.2 [1] (🅿)
1F202 FE0F ; Basic_Emoji ; Japanese service charge button # 6.0 [1] (🈂)
1F237 FE0F ; Basic_Emoji ; Japanese monthly amount button # 6.0 [1] (🈷)
1F321 FE0F ; Basic_Emoji ; thermometer # 7.0 [1] (🌡)
1F324 FE0F ; Basic_Emoji ; sun behind small cloud # 7.0 [1] (🌤)
1F325 FE0F ; Basic_Emoji ; sun behind large cloud # 7.0 [1] (🌥)
1F326 FE0F ; Basic_Emoji ; sun behind rain cloud # 7.0 [1] (🌦)
1F327 FE0F ; Basic_Emoji ; cloud with rain # 7.0 [1] (🌧)
1F328 FE0F ; Basic_Emoji ; cloud with snow # 7.0 [1] (🌨)
1F329 FE0F ; Basic_Emoji ; cloud with lightning # 7.0 [1] (🌩)
1F32A FE0F ; Basic_Emoji ; tornado # 7.0 [1] (🌪)
1F32B FE0F ; Basic_Emoji ; fog # 7.0 [1] (🌫)
1F32C FE0F ; Basic_Emoji ; wind face # 7.0 [1] (🌬)
1F336 FE0F ; Basic_Emoji ; hot pepper # 7.0 [1] (🌶)
1F37D FE0F ; Basic_Emoji ; fork and knife with plate # 7.0 [1] (🍽)
1F396 FE0F ; Basic_Emoji ; military medal # 7.0 [1] (🎖)
1F397 FE0F ; Basic_Emoji ; reminder ribbon # 7.0 [1] (🎗)
1F399 FE0F ; Basic_Emoji ; studio microphone # 7.0 [1] (🎙)
1F39A FE0F ; Basic_Emoji ; level slider # 7.0 [1] (🎚)
1F39B FE0F ; Basic_Emoji ; control knobs # 7.0 [1] (🎛)
1F39E FE0F ; Basic_Emoji ; film frames # 7.0 [1] (🎞)
1F39F FE0F ; Basic_Emoji ; admission tickets # 7.0 [1] (🎟)
1F3CB FE0F ; Basic_Emoji ; person lifting weights # 7.0 [1] (🏋)
1F3CC FE0F ; Basic_Emoji ; person golfing # 7.0 [1] (🏌)
1F3CD FE0F ; Basic_Emoji ; motorcycle # 7.0 [1] (🏍)
1F3CE FE0F ; Basic_Emoji ; racing car # 7.0 [1] (🏎)
1F3D4 FE0F ; Basic_Emoji ; snow-capped mountain # 7.0 [1] (🏔)
1F3D5 FE0F ; Basic_Emoji ; camping # 7.0 [1] (🏕)
1F3D6 FE0F ; Basic_Emoji ; beach with umbrella # 7.0 [1] (🏖)
1F3D7 FE0F ; Basic_Emoji ; building construction # 7.0 [1] (🏗)
1F3D8 FE0F ; Basic_Emoji ; houses # 7.0 [1] (🏘)
1F3D9 FE0F ; Basic_Emoji ; cityscape # 7.0 [1] (🏙)
1F3DA FE0F ; Basic_Emoji ; derelict house # 7.0 [1] (🏚)
1F3DB FE0F ; Basic_Emoji ; classical building # 7.0 [1] (🏛)
1F3DC FE0F ; Basic_Emoji ; desert # 7.0 [1] (🏜)
1F3DD FE0F ; Basic_Emoji ; desert island # 7.0 [1] (🏝)
1F3DE FE0F ; Basic_Emoji ; national park # 7.0 [1] (🏞)
1F3DF FE0F ; Basic_Emoji ; stadium # 7.0 [1] (🏟)
1F3F3 FE0F ; Basic_Emoji ; white flag # 7.0 [1] (🏳)
1F3F5 FE0F ; Basic_Emoji ; rosette # 7.0 [1] (🏵)
1F3F7 FE0F ; Basic_Emoji ; label # 7.0 [1] (🏷)
1F43F FE0F ; Basic_Emoji ; chipmunk # 7.0 [1] (🐿)
1F441 FE0F ; Basic_Emoji ; eye # 7.0 [1] (👁)
1F4FD FE0F ; Basic_Emoji ; film projector # 7.0 [1] (📽)
1F549 FE0F ; Basic_Emoji ; om # 7.0 [1] (🕉)
1F54A FE0F ; Basic_Emoji ; dove # 7.0 [1] (🕊)
1F56F FE0F ; Basic_Emoji ; candle # 7.0 [1] (🕯)
1F570 FE0F ; Basic_Emoji ; mantelpiece clock # 7.0 [1] (🕰)
1F573 FE0F ; Basic_Emoji ; hole # 7.0 [1] (🕳)
1F574 FE0F ; Basic_Emoji ; man in suit levitating # 7.0 [1] (🕴)
1F575 FE0F ; Basic_Emoji ; detective # 7.0 [1] (🕵)
1F576 FE0F ; Basic_Emoji ; sunglasses # 7.0 [1] (🕶)
1F577 FE0F ; Basic_Emoji ; spider # 7.0 [1] (🕷)
1F578 FE0F ; Basic_Emoji ; spider web # 7.0 [1] (🕸)
1F579 FE0F ; Basic_Emoji ; joystick # 7.0 [1] (🕹)
1F587 FE0F ; Basic_Emoji ; linked paperclips # 7.0 [1] (🖇)
1F58A FE0F ; Basic_Emoji ; pen # 7.0 [1] (🖊)
1F58B FE0F ; Basic_Emoji ; fountain pen # 7.0 [1] (🖋)
1F58C FE0F ; Basic_Emoji ; paintbrush # 7.0 [1] (🖌)
1F58D FE0F ; Basic_Emoji ; crayon # 7.0 [1] (🖍)
1F590 FE0F ; Basic_Emoji ; hand with fingers splayed # 7.0 [1] (🖐)
1F5A5 FE0F ; Basic_Emoji ; desktop computer # 7.0 [1] (🖥)
1F5A8 FE0F ; Basic_Emoji ; printer # 7.0 [1] (🖨)
1F5B1 FE0F ; Basic_Emoji ; computer mouse # 7.0 [1] (🖱)
1F5B2 FE0F ; Basic_Emoji ; trackball # 7.0 [1] (🖲)
1F5BC FE0F ; Basic_Emoji ; framed picture # 7.0 [1] (🖼)
1F5C2 FE0F ; Basic_Emoji ; card index dividers # 7.0 [1] (🗂)
1F5C3 FE0F ; Basic_Emoji ; card file box # 7.0 [1] (🗃)
1F5C4 FE0F ; Basic_Emoji ; file cabinet # 7.0 [1] (🗄)
1F5D1 FE0F ; Basic_Emoji ; wastebasket # 7.0 [1] (🗑)
1F5D2 FE0F ; Basic_Emoji ; spiral notepad # 7.0 [1] (🗒)
1F5D3 FE0F ; Basic_Emoji ; spiral calendar # 7.0 [1] (🗓)
1F5DC FE0F ; Basic_Emoji ; clamp # 7.0 [1] (🗜)
1F5DD FE0F ; Basic_Emoji ; old key # 7.0 [1] (🗝)
1F5DE FE0F ; Basic_Emoji ; rolled-up newspaper # 7.0 [1] (🗞)
1F5E1 FE0F ; Basic_Emoji ; dagger # 7.0 [1] (🗡)
1F5E3 FE0F ; Basic_Emoji ; speaking head # 7.0 [1] (🗣)
1F5E8 FE0F ; Basic_Emoji ; left speech bubble # 7.0 [1] (🗨)
1F5EF FE0F ; Basic_Emoji ; right anger bubble # 7.0 [1] (🗯)
1F5F3 FE0F ; Basic_Emoji ; ballot box with ballot # 7.0 [1] (🗳)
1F5FA FE0F ; Basic_Emoji ; world map # 7.0 [1] (🗺)
1F6CB FE0F ; Basic_Emoji ; couch and lamp # 7.0 [1] (🛋)
1F6CD FE0F ; Basic_Emoji ; shopping bags # 7.0 [1] (🛍)
1F6CE FE0F ; Basic_Emoji ; bellhop bell # 7.0 [1] (🛎)
1F6CF FE0F ; Basic_Emoji ; bed # 7.0 [1] (🛏)
1F6E0 FE0F ; Basic_Emoji ; hammer and wrench # 7.0 [1] (🛠)
1F6E1 FE0F ; Basic_Emoji ; shield # 7.0 [1] (🛡)
1F6E2 FE0F ; Basic_Emoji ; oil drum # 7.0 [1] (🛢)
1F6E3 FE0F ; Basic_Emoji ; motorway # 7.0 [1] (🛣)
1F6E4 FE0F ; Basic_Emoji ; railway track # 7.0 [1] (🛤)
1F6E5 FE0F ; Basic_Emoji ; motor boat # 7.0 [1] (🛥)
1F6E9 FE0F ; Basic_Emoji ; small airplane # 7.0 [1] (🛩)
1F6F0 FE0F ; Basic_Emoji ; satellite # 7.0 [1] (🛰)
1F6F3 FE0F ; Basic_Emoji ; passenger ship # 7.0 [1] (🛳)
*/

View File

@ -33,6 +33,7 @@
*/
use crate::datetime::UnixTimestamp;
use crate::email::address::StrBuild;
use crate::email::parser::BytesExt;
use crate::email::*;
@ -43,7 +44,6 @@ pub use iterators::*;
use crate::text_processing::grapheme_clusters::*;
use uuid::Uuid;
use std::cell::RefCell;
use std::cmp::Ordering;
use std::collections::{HashMap, HashSet};
use std::fmt;
@ -130,7 +130,7 @@ macro_rules! make {
e.parent = Some($p);
});
let old_group = std::mem::replace($threads.groups.entry(old_group_hash).or_default(), ThreadGroup::Node {
parent: RefCell::new(parent_group_hash),
parent: Arc::new(RwLock::new(parent_group_hash)),
});
$threads.thread_nodes.entry($c).and_modify(|e| {
e.group = parent_group_hash;
@ -291,7 +291,7 @@ pub struct Thread {
#[derive(Clone, Debug, Deserialize, Serialize)]
pub enum ThreadGroup {
Root(Thread),
Node { parent: RefCell<ThreadHash> },
Node { parent: Arc<RwLock<ThreadHash>> },
}
impl Default for ThreadGroup {
@ -337,6 +337,7 @@ impl Thread {
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct ThreadNode {
pub message: Option<EnvelopeHash>,
pub other_mailbox: bool,
pub parent: Option<ThreadNodeHash>,
pub children: Vec<ThreadNodeHash>,
pub date: UnixTimestamp,
@ -350,6 +351,7 @@ impl Default for ThreadNode {
ThreadNode {
message: None,
parent: None,
other_mailbox: false,
children: Vec::new(),
date: UnixTimestamp::default(),
show_subject: true,
@ -408,16 +410,16 @@ impl ThreadNode {
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct Threads {
pub thread_nodes: HashMap<ThreadNodeHash, ThreadNode>,
root_set: RefCell<Vec<ThreadNodeHash>>,
tree_index: RefCell<Vec<ThreadNodeHash>>,
root_set: Arc<RwLock<Vec<ThreadNodeHash>>>,
tree_index: Arc<RwLock<Vec<ThreadNodeHash>>>,
pub groups: HashMap<ThreadHash, ThreadGroup>,
message_ids: HashMap<Vec<u8>, ThreadNodeHash>,
pub message_ids_set: HashSet<Vec<u8>>,
pub missing_message_ids: HashSet<Vec<u8>>,
pub hash_set: HashSet<EnvelopeHash>,
sort: RefCell<(SortField, SortOrder)>,
subsort: RefCell<(SortField, SortOrder)>,
sort: Arc<RwLock<(SortField, SortOrder)>>,
subsort: Arc<RwLock<(SortField, SortOrder)>>,
}
impl PartialEq for ThreadNode {
@ -451,13 +453,13 @@ impl Threads {
pub fn find_group(&self, h: ThreadHash) -> ThreadHash {
let p = match self.groups[&h] {
ThreadGroup::Root(_) => return h,
ThreadGroup::Node { ref parent } => *parent.borrow(),
ThreadGroup::Node { ref parent } => *parent.read().unwrap(),
};
let parent_group = self.find_group(p);
match self.groups[&h] {
ThreadGroup::Node { ref parent } => {
*parent.borrow_mut() = parent_group;
*parent.write().unwrap() = parent_group;
}
_ => unreachable!(),
}
@ -488,8 +490,8 @@ impl Threads {
message_ids_set,
missing_message_ids,
hash_set,
sort: RefCell::new((SortField::Date, SortOrder::Desc)),
subsort: RefCell::new((SortField::Subject, SortOrder::Desc)),
sort: Arc::new(RwLock::new((SortField::Date, SortOrder::Desc))),
subsort: Arc::new(RwLock::new((SortField::Subject, SortOrder::Desc))),
..Default::default()
}
@ -570,7 +572,7 @@ impl Threads {
};
if self.thread_nodes[&t_id].parent.is_none() {
let mut tree_index = self.tree_index.borrow_mut();
let mut tree_index = self.tree_index.write().unwrap();
if let Some(i) = tree_index.iter().position(|t| *t == t_id) {
tree_index.remove(i);
}
@ -652,17 +654,36 @@ impl Threads {
env_hash: EnvelopeHash,
other_mailbox: bool,
) -> bool {
{
let envelopes_lck = envelopes.read().unwrap();
let message_id = envelopes_lck[&env_hash].message_id().raw();
if self.message_ids.contains_key(message_id)
&& !self.missing_message_ids.contains(message_id)
{
let thread_hash = self.message_ids[message_id];
let node = self.thread_nodes.entry(thread_hash).or_default();
drop(message_id);
drop(envelopes_lck);
envelopes
.write()
.unwrap()
.get_mut(&env_hash)
.unwrap()
.set_thread(thread_hash);
/* If thread node currently has a message from a foreign mailbox and env_hash is
* from current mailbox we want to update it, otherwise return */
if !(node.other_mailbox && !other_mailbox) {
return false;
}
}
}
let envelopes_lck = envelopes.read().unwrap();
let reply_to_id: Option<ThreadNodeHash> = envelopes_lck[&env_hash]
.in_reply_to()
.map(crate::email::StrBuild::raw)
.map(StrBuild::raw)
.and_then(|r| self.message_ids.get(r).cloned());
let message_id = envelopes_lck[&env_hash].message_id().raw();
if self.message_ids_set.contains(message_id)
&& !self.missing_message_ids.contains(message_id)
{
return false;
}
if other_mailbox
&& reply_to_id.is_none()
@ -686,13 +707,14 @@ impl Threads {
None
},
)
.unwrap_or_else(ThreadNodeHash::new);
.unwrap_or_else(|| ThreadNodeHash::from(message_id));
{
let mut node = self.thread_nodes.entry(new_id).or_default();
node.message = Some(env_hash);
if node.parent.is_none() {
node.parent = reply_to_id;
}
node.other_mailbox = other_mailbox;
node.date = envelopes_lck[&env_hash].date();
node.unseen = !envelopes_lck[&env_hash].is_seen();
}
@ -739,11 +761,8 @@ impl Threads {
self.hash_set.insert(env_hash);
if let Some(reply_to_id) = reply_to_id {
make!((reply_to_id) parent of (new_id), self);
} else if let Some(r) = envelopes_lck[&env_hash]
.in_reply_to()
.map(crate::email::StrBuild::raw)
{
let reply_to_id = ThreadNodeHash::new();
} else if let Some(r) = envelopes_lck[&env_hash].in_reply_to().map(StrBuild::raw) {
let reply_to_id = ThreadNodeHash::from(r);
self.thread_nodes.insert(
reply_to_id,
ThreadNode {
@ -787,7 +806,7 @@ impl Threads {
make!((id) parent of (current_descendant_id), self);
current_descendant_id = id;
} else {
let id = ThreadNodeHash::new();
let id = ThreadNodeHash::from(reference.raw());
self.thread_nodes.insert(
id,
ThreadNode {
@ -825,7 +844,7 @@ impl Threads {
/*
save_graph(
&self.tree_index.borrow(),
&self.tree_index.read().unwrap(),
&self.thread_nodes,
&self
.message_ids
@ -851,7 +870,7 @@ impl Threads {
ref thread_nodes,
..
} = self;
let tree = &mut tree_index.borrow_mut();
let tree = &mut tree_index.write().unwrap();
for t in tree.iter_mut() {
thread_nodes[t].children.sort_by(|a, b| match subsort {
(SortField::Date, SortOrder::Desc) => {
@ -1070,7 +1089,7 @@ impl Threads {
});
}
fn inner_sort_by(&self, sort: (SortField, SortOrder), envelopes: &Envelopes) {
let tree = &mut self.tree_index.borrow_mut();
let tree = &mut self.tree_index.write().unwrap();
let envelopes = envelopes.read().unwrap();
tree.sort_by(|a, b| match sort {
(SortField::Date, SortOrder::Desc) => {
@ -1152,13 +1171,13 @@ impl Threads {
subsort: (SortField, SortOrder),
envelopes: &Envelopes,
) {
if *self.sort.borrow() != sort {
if *self.sort.read().unwrap() != sort {
self.inner_sort_by(sort, envelopes);
*self.sort.borrow_mut() = sort;
*self.sort.write().unwrap() = sort;
}
if *self.subsort.borrow() != subsort {
if *self.subsort.read().unwrap() != subsort {
self.inner_subsort_by(subsort, envelopes);
*self.subsort.borrow_mut() = subsort;
*self.subsort.write().unwrap() = subsort;
}
}
@ -1176,11 +1195,11 @@ impl Threads {
}
pub fn root_len(&self) -> usize {
self.tree_index.borrow().len()
self.tree_index.read().unwrap().len()
}
pub fn root_set(&self, idx: usize) -> ThreadNodeHash {
self.tree_index.borrow()[idx]
self.tree_index.read().unwrap()[idx]
}
pub fn roots(&self) -> SmallVec<[ThreadHash; 1024]> {

View File

@ -49,9 +49,6 @@ static GLOBAL: System = System;
extern crate melib;
use melib::*;
mod unix;
use unix::*;
#[macro_use]
pub mod types;
use crate::types::*;
@ -79,7 +76,6 @@ pub mod sqlite3;
pub mod jobs;
pub mod mailcap;
pub mod plugins;
use std::os::raw::c_int;
@ -105,7 +101,6 @@ fn notify(
nix::fcntl::FcntlArg::F_SETFL(nix::fcntl::OFlag::O_NONBLOCK),
);
std::thread::spawn(move || {
let mut buf = [0; 1];
let mut ctr = 0;
loop {
ctr %= 3;
@ -118,16 +113,6 @@ fn notify(
for signal in signals.pending() {
let _ = s.send_timeout(signal, Duration::from_millis(500)).ok();
}
while nix::unistd::read(alarm_pipe_r, buf.as_mut())
.map(|s| s > 0)
.unwrap_or(false)
{
let value = buf[0];
let _ = sender.send_timeout(
ThreadEvent::UIEvent(UIEvent::Timer(value)),
Duration::from_millis(2000),
);
}
std::thread::sleep(std::time::Duration::from_millis(100));
ctr += 1;
@ -150,7 +135,7 @@ fn parse_manpage(src: &str) -> Result<ManPages> {
use structopt::StructOpt;
#[derive(Debug)]
#[derive(Copy, Clone, Debug)]
/// Choose manpage
enum ManPages {
/// meli(1)
@ -206,6 +191,9 @@ enum SubCommand {
struct ManOpt {
#[structopt(default_value = "meli", possible_values=&["meli", "conf", "themes"], value_name="PAGE", parse(try_from_str = parse_manpage))]
page: ManPages,
/// If true, output text in stdout instead of spawning $PAGER.
#[structopt(long = "no-raw", alias = "no-raw", value_name = "bool")]
no_raw: Option<Option<bool>>,
}
fn main() {
@ -251,13 +239,51 @@ fn run_app(opt: Opt) -> Result<()> {
}
#[cfg(feature = "cli-docs")]
Some(SubCommand::Man(manopt)) => {
let _page = manopt.page;
const MANPAGES: [&'static str; 3] = [
include_str!(concat!(env!("OUT_DIR"), "/meli.txt")),
include_str!(concat!(env!("OUT_DIR"), "/meli.conf.txt")),
include_str!(concat!(env!("OUT_DIR"), "/meli-themes.txt")),
let ManOpt { page, no_raw } = manopt;
const MANPAGES: [&'static [u8]; 3] = [
include_bytes!(concat!(env!("OUT_DIR"), "/meli.txt.gz")),
include_bytes!(concat!(env!("OUT_DIR"), "/meli.conf.txt.gz")),
include_bytes!(concat!(env!("OUT_DIR"), "/meli-themes.txt.gz")),
];
println!("{}", MANPAGES[_page as usize]);
use flate2::bufread::GzDecoder;
use std::io::prelude::*;
let mut gz = GzDecoder::new(MANPAGES[page as usize]);
let mut v = String::with_capacity(
str::parse::<usize>(unsafe {
std::str::from_utf8_unchecked(gz.header().unwrap().comment().unwrap())
})
.expect(&format!(
"{:?} was not compressed with size comment header",
page
)),
);
gz.read_to_string(&mut v)?;
if let Some(no_raw) = no_raw {
match no_raw {
Some(true) => {}
None if (unsafe { libc::isatty(libc::STDOUT_FILENO) == 1 }) => {}
Some(false) | None => {
println!("{}", &v);
return Ok(());
}
}
} else {
if unsafe { libc::isatty(libc::STDOUT_FILENO) != 1 } {
println!("{}", &v);
return Ok(());
}
}
use std::process::{Command, Stdio};
let mut handle = Command::new(std::env::var("PAGER").unwrap_or("more".to_string()))
.stdin(Stdio::piped())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.spawn()?;
handle.stdin.take().unwrap().write_all(v.as_bytes())?;
handle.wait()?;
return Ok(());
}
#[cfg(not(feature = "cli-docs"))]
@ -324,20 +350,18 @@ fn run_app(opt: Opt) -> Result<()> {
vec![
Box::new(listing::Listing::new(&mut state.context)),
Box::new(ContactList::new(&state.context)),
Box::new(StatusPanel::new(crate::conf::value(
&state.context,
"theme_default",
))),
],
&state.context,
));
let status_bar = Box::new(StatusBar::new(window));
let status_bar = Box::new(StatusBar::new(&state.context, window));
state.register_component(status_bar);
#[cfg(feature = "dbus-notifications")]
#[cfg(all(target_os = "linux", feature = "dbus-notifications"))]
{
let dbus_notifications = Box::new(components::notifications::DbusNotifications::new());
let dbus_notifications = Box::new(components::notifications::DbusNotifications::new(
&state.context,
));
state.register_component(dbus_notifications);
}
state.register_component(Box::new(
@ -351,6 +375,7 @@ fn run_app(opt: Opt) -> Result<()> {
.general
.enter_command_mode
.clone();
let quit_key: Key = state.context.settings.shortcuts.general.quit.clone();
/* Keep track of the input mode. See UIMode for details */
'main: loop {
@ -397,7 +422,7 @@ fn run_app(opt: Opt) -> Result<()> {
match state.mode {
UIMode::Normal => {
match k {
Key::Char('q') | Key::Char('Q') => {
_ if k == quit_key => {
if state.can_quit_cleanly() {
drop(state);
break 'main;
@ -418,8 +443,7 @@ fn run_app(opt: Opt) -> Result<()> {
},
UIMode::Insert => {
match k {
Key::Char('\n') | Key::Esc => {
state.mode = UIMode::Normal;
Key::Esc => {
state.rcv_event(UIEvent::ChangeMode(UIMode::Normal));
state.redraw();
},

View File

@ -57,7 +57,7 @@ macro_rules! to_stream {
};
($($tokens:expr),*) => {
TokenStream {
tokens: &[$($token),*],
tokens: &[$($tokens),*],
}
};
}
@ -303,6 +303,21 @@ define_commands!([
}
)
},
{ tags: ["import "],
desc: "import FILESYSTEM_PATH MAILBOX_PATH",
tokens: &[One(Literal("import")), One(Filepath), One(MailboxPath)],
parser:(
fn import(input: &[u8]) -> IResult<&[u8], Action> {
let (input, _) = tag("import")(input.trim())?;
let (input, _) = is_a(" ")(input)?;
let (input, file) = quoted_argument(input)?;
let (input, _) = is_a(" ")(input)?;
let (input, mailbox_path) = quoted_argument(input)?;
let (input, _) = eof(input)?;
Ok((input, Listing(Import(file.to_string().into(), mailbox_path.to_string()))))
}
)
},
{ tags: ["close"],
desc: "close non-sticky tabs",
tokens: &[One(Literal("close"))],
@ -365,14 +380,16 @@ define_commands!([
}
)
},
{ tags: ["toggle_thread_snooze"],
{ tags: ["toggle thread_snooze"],
desc: "turn off new notifications for this thread",
tokens: &[One(Literal("toggle_thread_snooze"))],
tokens: &[One(Literal("toggle thread_snooze"))],
parser: (
fn toggle_thread_snooze(input: &[u8]) -> IResult<&[u8], Action> {
let (input, _) = tag("toggle_thread_snooze")(input.trim())?;
let (input, _) = tag("toggle")(input)?;
let (input, _) = is_a(" ")(input)?;
let (input, _) = tag("thread_snooze")(input)?;
let (input, _) = eof(input)?;
Ok((input, ToggleThreadSnooze))
Ok((input, Listing(ToggleThreadSnooze)))
}
)
},
@ -402,6 +419,19 @@ define_commands!([
}
)
},
{ tags: ["export-mbox "],
desc: "export-mbox PATH",
tokens: &[One(Literal("export-mbox")), One(Filepath)],
parser:(
fn export_mbox(input: &[u8]) -> IResult<&[u8], Action> {
let (input, _) = tag("export-mbox")(input.trim())?;
let (input, _) = is_a(" ")(input)?;
let (input, path) = quoted_argument(input.trim())?;
let (input, _) = eof(input)?;
Ok((input, Listing(ExportMbox(Some(melib::backends::mbox::MboxFormat::MboxCl2), path.to_string().into()))))
}
)
},
{ tags: ["list-archive", "list-post", "list-unsubscribe", "list-"],
desc: "list-[unsubscribe/post/archive]",
tokens: &[One(Alternatives(&[to_stream!(One(Literal("list-archive"))), to_stream!(One(Literal("list-post"))), to_stream!(One(Literal("list-unsubscribe")))]))],
@ -480,9 +510,10 @@ define_commands!([
}
)
},
{ tags: ["add-attachment "],
{ tags: ["add-attachment ", "add-attachment-file-picker "],
desc: "add-attachment PATH",
tokens: &[One(Literal("add-attachment")), One(Filepath)],
tokens: &[One(
Alternatives(&[to_stream!(One(Literal("add-attachment")), One(Filepath)), to_stream!(One(Literal("add-attachment-file-picker")))]))],
parser:(
fn add_attachment<'a>(input: &'a [u8]) -> IResult<&'a [u8], Action> {
alt((
@ -500,6 +531,18 @@ define_commands!([
let (input, path) = quoted_argument(input)?;
let (input, _) = eof(input)?;
Ok((input, Compose(AddAttachment(path.to_string()))))
}, |input: &'a [u8]| -> IResult<&'a [u8], Action> {
let (input, _) = tag("add-attachment-file-picker")(input.trim())?;
let (input, _) = eof(input)?;
Ok((input, Compose(AddAttachmentFilePicker(None))))
}, |input: &'a [u8]| -> IResult<&'a [u8], Action> {
let (input, _) = tag("add-attachment-file-picker")(input.trim())?;
let (input, _) = is_a(" ")(input)?;
let (input, _) = tag("<")(input.trim())?;
let (input, _) = is_a(" ")(input)?;
let (input, shell) = map_res(not_line_ending, std::str::from_utf8)(input)?;
let (input, _) = eof(input)?;
Ok((input, Compose(AddAttachmentFilePicker(Some(shell.to_string())))))
}
))(input)
}
@ -542,6 +585,19 @@ define_commands!([
}
)
},
{ tags: ["toggle encrypt"],
desc: "toggle encryption for this draft",
tokens: &[One(Literal("toggle")), One(Literal("encrypt"))],
parser:(
fn toggle_encrypt(input: &[u8]) -> IResult<&[u8], Action> {
let (input, _) = tag("toggle")(input)?;
let (input, _) = is_a(" ")(input)?;
let (input, _) = tag("encrypt")(input)?;
let (input, _) = eof(input)?;
Ok((input, Compose(ToggleEncrypt)))
}
)
},
{ tags: ["create-mailbox "],
desc: "create-mailbox ACCOUNT MAILBOX_PATH",
tokens: &[One(Literal("create-mailbox")), One(AccountName), One(MailboxPath)],
@ -658,6 +714,19 @@ define_commands!([
}
)
},
{ tags: ["export-mail "],
desc: "export-mail PATH",
tokens: &[One(Literal("export-mail")), One(Filepath)],
parser:(
fn export_mail(input: &[u8]) -> IResult<&[u8], Action> {
let (input, _) = tag("export-mail")(input.trim())?;
let (input, _) = is_a(" ")(input)?;
let (input, path) = quoted_argument(input.trim())?;
let (input, _) = eof(input)?;
Ok((input, View(ExportMail(path.to_string()))))
}
)
},
{ tags: ["tag", "tag add", "tag remove"],
desc: "tag [add/remove], edits message's tags.",
tokens: &[One(Literal("tag")), One(Alternatives(&[to_stream!(One(Literal("add"))), to_stream!(One(Literal("remove")))]))],
@ -710,6 +779,41 @@ define_commands!([
Ok((input, PrintSetting(setting.to_string())))
}
)
},
{ tags: ["toggle mouse"],
desc: "toggle mouse support",
tokens: &[One(Literal("toggle")), One(Literal("mouse"))],
parser:(
fn toggle_mouse(input: &[u8]) -> IResult<&[u8], Action> {
let (input, _) = tag("toggle")(input)?;
let (input, _) = is_a(" ")(input)?;
let (input, _) = tag("mouse")(input)?;
let (input, _) = eof(input)?;
Ok((input, ToggleMouse))
}
)
},
{ tags: ["quit"],
desc: "quit meli",
tokens: &[One(Literal("quit"))],
parser:(
fn quit(input: &[u8]) -> IResult<&[u8], Action> {
let (input, _) = tag("quit")(input.trim())?;
let (input, _) = eof(input.trim())?;
Ok((input, Quit))
}
)
},
{ tags: ["reload-config"],
desc: "reload configuration file",
tokens: &[One(Literal("reload-config"))],
parser:(
fn reload_config(input: &[u8]) -> IResult<&[u8], Action> {
let (input, _) = tag("reload-config")(input.trim())?;
let (input, _) = eof(input.trim())?;
Ok((input, ReloadConfiguration))
}
)
}
]);
@ -756,16 +860,24 @@ fn listing_action(input: &[u8]) -> IResult<&[u8], Action> {
seen_flag,
delete_message,
copymove,
import,
search,
select,
toggle_thread_snooze,
open_in_new_tab,
export_mbox,
_tag,
))(input)
}
fn compose_action(input: &[u8]) -> IResult<&[u8], Action> {
alt((add_attachment, remove_attachment, toggle_sign, save_draft))(input)
alt((
add_attachment,
remove_attachment,
toggle_sign,
toggle_encrypt,
save_draft,
))(input)
}
fn account_action(input: &[u8]) -> IResult<&[u8], Action> {
@ -773,7 +885,7 @@ fn account_action(input: &[u8]) -> IResult<&[u8], Action> {
}
fn view(input: &[u8]) -> IResult<&[u8], Action> {
alt((pipe, save_attachment))(input)
alt((pipe, save_attachment, export_mail))(input)
}
pub fn parse_command(input: &[u8]) -> Result<Action, MeliError> {
@ -795,6 +907,9 @@ pub fn parse_command(input: &[u8]) -> Result<Action, MeliError> {
rename_mailbox,
account_action,
print_setting,
toggle_mouse,
reload_config,
quit,
))(input)
.map(|(_, v)| v)
.map_err(|err| err.into())

View File

@ -25,6 +25,7 @@
use crate::components::Component;
pub use melib::thread::{SortField, SortOrder};
use std::path::PathBuf;
extern crate uuid;
use uuid::Uuid;
@ -49,9 +50,12 @@ pub enum ListingAction {
CopyToOtherAccount(AccountName, MailboxPath),
MoveTo(MailboxPath),
MoveToOtherAccount(AccountName, MailboxPath),
Import(PathBuf, MailboxPath),
ExportMbox(Option<melib::backends::mbox::MboxFormat>, PathBuf),
Delete,
OpenInNewTab,
Tag(TagAction),
ToggleThreadSnooze,
}
#[derive(Debug)]
@ -72,15 +76,18 @@ pub enum MailingListAction {
pub enum ViewAction {
Pipe(String, Vec<String>),
SaveAttachment(usize, String),
ExportMail(String),
}
#[derive(Debug)]
pub enum ComposeAction {
AddAttachment(String),
AddAttachmentFilePicker(Option<String>),
AddAttachmentPipe(String),
RemoveAttachment(usize),
SaveDraft,
ToggleSign,
ToggleEncrypt,
}
#[derive(Debug)]
@ -107,7 +114,6 @@ pub enum Action {
Sort(SortField, SortOrder),
SubSort(SortField, SortOrder),
Tab(TabAction),
ToggleThreadSnooze,
MailingListAction(MailingListAction),
View(ViewAction),
SetEnv(String, String),
@ -116,6 +122,9 @@ pub enum Action {
Mailbox(AccountName, MailboxOperation),
AccountAction(AccountName, AccountAction),
PrintSetting(String),
ReloadConfiguration,
ToggleMouse,
Quit,
}
impl Action {
@ -126,7 +135,6 @@ impl Action {
Action::Sort(_, _) => false,
Action::SubSort(_, _) => false,
Action::Tab(_) => false,
Action::ToggleThreadSnooze => false,
Action::MailingListAction(_) => true,
Action::View(_) => false,
Action::SetEnv(_, _) => false,
@ -135,6 +143,9 @@ impl Action {
Action::Mailbox(_, _) => true,
Action::AccountAction(_, _) => false,
Action::PrintSetting(_) => false,
Action::ToggleMouse => false,
Action::Quit => true,
Action::ReloadConfiguration => false,
}
}
}

View File

@ -54,6 +54,34 @@ pub type ComponentId = Uuid;
pub type ShortcutMap = IndexMap<&'static str, Key>;
pub type ShortcutMaps = IndexMap<&'static str, ShortcutMap>;
#[derive(Debug, Clone, Copy)]
pub enum PageMovement {
Up(usize),
Right(usize),
Left(usize),
Down(usize),
PageUp(usize),
PageDown(usize),
Home,
End,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ScrollContext {
shown_lines: usize,
total_lines: usize,
has_more_lines: bool,
}
#[derive(Debug, Clone, Copy)]
pub enum ScrollUpdate {
End(ComponentId),
Update {
id: ComponentId,
context: ScrollContext,
},
}
/// Types implementing this Trait can draw on the terminal and receive events.
/// If a type wants to skip drawing if it has not changed anything, it can hold some flag in its
/// fields (eg self.dirty = false) and act upon that in their `draw` implementation.

View File

@ -40,7 +40,7 @@ pub struct ContactManager {
parent_id: ComponentId,
pub card: Card,
mode: ViewMode,
form: FormWidget,
form: FormWidget<bool>,
account_pos: usize,
content: CellBuffer,
theme_default: ThemeAttribute,
@ -59,13 +59,6 @@ impl fmt::Display for ContactManager {
impl ContactManager {
fn new(context: &Context) -> Self {
let theme_default: ThemeAttribute = crate::conf::value(context, "theme_default");
let default_cell = {
let mut ret = Cell::with_char(' ');
ret.set_fg(theme_default.fg)
.set_bg(theme_default.bg)
.set_attrs(theme_default.attrs);
ret
};
ContactManager {
id: Uuid::nil(),
parent_id: Uuid::nil(),
@ -73,7 +66,7 @@ impl ContactManager {
mode: ViewMode::Edit,
form: FormWidget::default(),
account_pos: 0,
content: CellBuffer::new(100, 1, default_cell),
content: CellBuffer::new_with_context(100, 1, None, context),
theme_default,
dirty: true,
has_changes: false,
@ -105,8 +98,7 @@ impl ContactManager {
if self.card.external_resource() {
self.mode = ViewMode::ReadOnly;
self.content
.resize(self.content.size().0, 2, Cell::default());
let _ = self.content.resize(self.content.size().0, 2, None);
write_string_to_grid(
"This contact's origin is external and cannot be edited within meli.",
&mut self.content,
@ -118,24 +110,30 @@ impl ContactManager {
);
}
self.form = FormWidget::new("Save".into());
self.form = FormWidget::new(("Save".into(), true));
self.form.add_button(("Cancel(Esc)".into(), false));
self.form
.push(("NAME".into(), self.card.name().to_string()));
.push(("NAME".into(), self.card.name().to_string().into()));
self.form.push((
"ADDITIONAL NAME".into(),
self.card.additionalname().to_string(),
self.card.additionalname().to_string().into(),
));
self.form.push((
"NAME PREFIX".into(),
self.card.name_prefix().to_string().into(),
));
self.form.push((
"NAME SUFFIX".into(),
self.card.name_suffix().to_string().into(),
));
self.form
.push(("NAME PREFIX".into(), self.card.name_prefix().to_string()));
.push(("E-MAIL".into(), self.card.email().to_string().into()));
self.form
.push(("NAME SUFFIX".into(), self.card.name_suffix().to_string()));
.push(("URL".into(), self.card.url().to_string().into()));
self.form
.push(("E-MAIL".into(), self.card.email().to_string()));
self.form.push(("URL".into(), self.card.url().to_string()));
self.form.push(("KEY".into(), self.card.key().to_string()));
.push(("KEY".into(), self.card.key().to_string().into()));
for (k, v) in self.card.extra_properties() {
self.form.push((k.to_string(), v.to_string()));
self.form.push((k.to_string().into(), v.to_string().into()));
}
}
@ -173,7 +171,7 @@ impl Component for ContactManager {
match self.mode {
ViewMode::Discard(ref mut selector) => {
/* Let user choose whether to quit with/without saving or cancel */
selector.draw(grid, center_area(area, selector.content.size()), context);
selector.draw(grid, area, context);
}
_ => {}
}
@ -182,6 +180,15 @@ impl Component for ContactManager {
}
fn process_event(&mut self, event: &mut UIEvent, context: &mut Context) -> bool {
match event {
UIEvent::ConfigReload { old_settings: _ } => {
self.theme_default = crate::conf::value(context, "theme_default");
self.content = CellBuffer::new_with_context(100, 1, None, context);
self.initialized = false;
self.set_dirty(true);
}
_ => {}
}
match self.mode {
ViewMode::Discard(ref mut selector) => {
if selector.process_event(event, context) {
@ -209,10 +216,10 @@ impl Component for ContactManager {
.into_iter()
.map(|(s, v)| {
(
s,
s.to_string(),
match v {
Field::Text(v, _) => v.as_str().to_string(),
Field::Choice(mut v, c) => v.remove(c),
Field::Choice(mut v, c) => v.remove(c).to_string(),
},
)
})

View File

@ -54,7 +54,10 @@ pub struct ContactList {
mode: ViewMode,
dirty: bool,
show_divider: bool,
sidebar_divider: char,
sidebar_divider_theme: ThemeAttribute,
menu_visibility: bool,
movement: Option<PageMovement>,
cmd_buf: String,
@ -98,7 +101,8 @@ impl ContactList {
cmd_buf: String::with_capacity(8),
view: None,
ratio: 90,
show_divider: false,
sidebar_divider: context.settings.listing.sidebar_divider,
sidebar_divider_theme: conf::value(context, "mail.sidebar_divider"),
menu_visibility: true,
id: ComponentId::new_v4(),
}
@ -132,25 +136,18 @@ impl ContactList {
min_width.2 = cmp::max(min_width.2, c.url().split_graphemes().len());
}
let default_cell = {
let mut ret = Cell::with_char(' ');
ret.set_fg(self.theme_default.fg)
.set_bg(self.theme_default.bg)
.set_attrs(self.theme_default.attrs);
ret
};
/* name column */
self.data_columns.columns[0] =
CellBuffer::new_with_context(min_width.0, self.length, default_cell, context);
CellBuffer::new_with_context(min_width.0, self.length, None, context);
/* email column */
self.data_columns.columns[1] =
CellBuffer::new_with_context(min_width.1, self.length, default_cell, context);
CellBuffer::new_with_context(min_width.1, self.length, None, context);
/* url column */
self.data_columns.columns[2] =
CellBuffer::new_with_context(min_width.2, self.length, default_cell, context);
CellBuffer::new_with_context(min_width.2, self.length, None, context);
/* source column */
self.data_columns.columns[3] =
CellBuffer::new_with_context("external".len(), self.length, default_cell, context);
CellBuffer::new_with_context("external".len(), self.length, None, context);
let account = &context.accounts[self.account_pos];
let book = &account.address_book;
@ -205,16 +202,9 @@ impl ContactList {
}
if self.length == 0 {
let default_cell = {
let mut ret = Cell::with_char(' ');
ret.set_fg(self.theme_default.fg)
.set_bg(self.theme_default.bg)
.set_attrs(self.theme_default.attrs);
ret
};
let message = "Address book is empty.".to_string();
self.data_columns.columns[0] =
CellBuffer::new_with_context(message.len(), self.length, default_cell, context);
CellBuffer::new_with_context(message.len(), self.length, None, context);
write_string_to_grid(
&message,
&mut self.data_columns.columns[0],
@ -239,13 +229,11 @@ impl ContactList {
change_colors(grid, area, fg_color, bg_color);
}
fn draw_menu(&mut self, grid: &mut CellBuffer, mut area: Area, context: &mut Context) {
fn draw_menu(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
if !self.is_dirty() {
return;
}
clear_area(grid, area, self.theme_default);
/* visually divide menu and listing */
area = (area.0, pos_dec(area.1, (1, 0)));
let upper_left = upper_left!(area);
let bottom_right = bottom_right!(area);
self.dirty = false;
@ -273,28 +261,35 @@ impl ContactList {
let width = width!(area);
let must_highlight_account: bool = self.account_pos == a.index;
let (fg_color, bg_color) = if must_highlight_account {
if self.account_pos == a.index {
(Color::Byte(233), Color::Byte(15))
} else {
(Color::Byte(15), Color::Byte(233))
let account_attrs = if must_highlight_account {
let mut v = crate::conf::value(context, "mail.sidebar_highlighted");
if !context.settings.terminal.use_color() {
v.attrs |= Attr::REVERSE;
}
v
} else {
(self.theme_default.fg, self.theme_default.bg)
crate::conf::value(context, "mail.sidebar_account_name")
};
let s = format!(" [{}]", context.accounts[a.index].address_book.len());
if a.name.grapheme_len() + s.len() > width + 1 {
/* Print account name */
let (x, y) =
write_string_to_grid(&a.name, grid, fg_color, bg_color, Attr::BOLD, area, None);
let (x, y) = write_string_to_grid(
&a.name,
grid,
account_attrs.fg,
account_attrs.bg,
account_attrs.attrs,
area,
None,
);
write_string_to_grid(
&s,
grid,
fg_color,
bg_color,
Attr::BOLD,
account_attrs.fg,
account_attrs.bg,
account_attrs.attrs,
(
pos_dec(
(get_x(bottom_right!(area)), get_y(upper_left!(area))),
@ -307,9 +302,9 @@ impl ContactList {
write_string_to_grid(
"",
grid,
fg_color,
bg_color,
Attr::BOLD,
account_attrs.fg,
account_attrs.bg,
account_attrs.attrs,
(
pos_dec(
(get_x(bottom_right!(area)), get_y(upper_left!(area))),
@ -321,20 +316,29 @@ impl ContactList {
);
for x in x..=get_x(bottom_right!(area)) {
grid[(x, y)].set_fg(fg_color);
grid[(x, y)].set_bg(bg_color);
grid[(x, y)]
.set_fg(account_attrs.fg)
.set_bg(account_attrs.bg)
.set_attrs(account_attrs.attrs);
}
} else {
/* Print account name */
let (x, y) =
write_string_to_grid(&a.name, grid, fg_color, bg_color, Attr::BOLD, area, None);
let (x, y) = write_string_to_grid(
&a.name,
grid,
account_attrs.fg,
account_attrs.bg,
account_attrs.attrs,
area,
None,
);
write_string_to_grid(
&s,
grid,
fg_color,
bg_color,
Attr::BOLD,
account_attrs.fg,
account_attrs.bg,
account_attrs.attrs,
(
pos_dec(
(get_x(bottom_right!(area)), get_y(upper_left!(area))),
@ -345,8 +349,10 @@ impl ContactList {
None,
);
for x in x..=get_x(bottom_right!(area)) {
grid[(x, y)].set_fg(fg_color);
grid[(x, y)].set_bg(bg_color);
grid[(x, y)]
.set_fg(account_attrs.fg)
.set_bg(account_attrs.bg)
.set_attrs(account_attrs.attrs);
}
}
}
@ -408,6 +414,27 @@ impl ContactList {
let top_idx = page_no * rows;
if self.length >= rows {
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate(
ScrollUpdate::Update {
id: self.id,
context: ScrollContext {
shown_lines: top_idx + rows,
total_lines: self.length,
has_more_lines: false,
},
},
)));
} else {
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate(
ScrollUpdate::End(self.id),
)));
}
/* If cursor position has changed, remove the highlight from the previous position and
* apply it in the new one. */
if self.cursor_pos != self.new_cursor_pos && prev_page_no == page_no {
@ -562,19 +589,12 @@ impl Component for ContactList {
};
let mid = get_x(bottom_right) - right_component_width;
if self.dirty && mid != get_x(upper_left) {
if self.show_divider {
for i in get_y(upper_left)..=get_y(bottom_right) {
grid[(mid, i)]
.set_ch(VERT_BOUNDARY)
.set_fg(self.theme_default.fg)
.set_bg(self.theme_default.bg);
}
} else {
for i in get_y(upper_left)..=get_y(bottom_right) {
grid[(mid, i)]
.set_fg(self.theme_default.fg)
.set_bg(self.theme_default.bg);
}
for i in get_y(upper_left)..=get_y(bottom_right) {
grid[(mid, i)]
.set_ch(self.sidebar_divider)
.set_fg(self.sidebar_divider_theme.fg)
.set_bg(self.sidebar_divider_theme.bg)
.set_attrs(self.sidebar_divider_theme.attrs);
}
context
.dirty_areas
@ -586,13 +606,25 @@ impl Component for ContactList {
} else if right_component_width == 0 {
self.draw_menu(grid, area, context);
} else {
self.draw_menu(grid, (upper_left, (mid, get_y(bottom_right))), context);
self.draw_menu(
grid,
(upper_left, (mid.saturating_sub(1), get_y(bottom_right))),
context,
);
self.draw_list(grid, (set_x(upper_left, mid + 1), bottom_right), context);
}
self.dirty = false;
}
fn process_event(&mut self, event: &mut UIEvent, context: &mut Context) -> bool {
if let UIEvent::ConfigReload { old_settings: _ } = event {
self.theme_default = crate::conf::value(context, "theme_default");
self.initialized = false;
self.sidebar_divider = context.settings.listing.sidebar_divider;
self.sidebar_divider_theme = conf::value(context, "mail.sidebar_divider");
self.set_dirty(true);
}
if let Some(ref mut v) = self.view {
if v.process_event(event, context) {
return true;
@ -610,6 +642,11 @@ impl Component for ContactList {
self.mode = ViewMode::View(manager.id());
self.view = Some(manager);
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate(
ScrollUpdate::End(self.id),
)));
return true;
}
@ -628,6 +665,11 @@ impl Component for ContactList {
self.mode = ViewMode::View(manager.id());
self.view = Some(manager);
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate(
ScrollUpdate::End(self.id),
)));
return true;
}
@ -642,7 +684,7 @@ impl Component for ContactList {
let mut draft: Draft = Draft::default();
*draft.headers_mut().get_mut("To").unwrap() =
format!("{} <{}>", &card.name(), &card.email());
let mut composer = Composer::new(account_hash, context);
let mut composer = Composer::with_account(account_hash, context);
composer.set_draft(draft);
context
.replies

View File

@ -33,6 +33,7 @@ pub use crate::view::*;
mod compose;
pub use self::compose::*;
#[cfg(feature = "gpgme")]
pub mod pgp;
mod status;

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,312 @@
/*
* meli -
*
* Copyright Manos Pitsidianakis
*
* This file is part of meli.
*
* meli is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* meli is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
use super::*;
#[derive(Debug, Copy, Clone, PartialEq)]
pub enum EditAttachmentCursor {
AttachmentNo(usize),
Buttons,
}
impl Default for EditAttachmentCursor {
fn default() -> Self {
EditAttachmentCursor::Buttons
}
}
#[derive(Debug)]
pub enum EditAttachmentMode {
Overview,
Edit {
inner: FormWidget<FormButtonActions>,
no: usize,
},
}
#[derive(Debug)]
pub struct EditAttachments {
pub mode: EditAttachmentMode,
pub buttons: ButtonWidget<FormButtonActions>,
pub cursor: EditAttachmentCursor,
pub dirty: bool,
pub id: ComponentId,
}
impl EditAttachments {
pub fn new() -> Self {
let mut buttons = ButtonWidget::new(("Add".into(), FormButtonActions::Other("add")));
buttons.push(("Go Back".into(), FormButtonActions::Cancel));
buttons.set_focus(true);
buttons.set_cursor(1);
EditAttachments {
mode: EditAttachmentMode::Overview,
buttons,
cursor: EditAttachmentCursor::Buttons,
dirty: true,
id: ComponentId::new_v4(),
}
}
}
impl EditAttachmentsRefMut<'_, '_> {
fn new_edit_widget(&self, no: usize) -> Option<FormWidget<FormButtonActions>> {
if no >= self.draft.attachments().len() {
return None;
}
let filename = self.draft.attachments()[no].content_type().name();
let mime_type = self.draft.attachments()[no].content_type();
let mut ret = FormWidget::new(("Save".into(), FormButtonActions::Accept));
ret.add_button(("Reset".into(), FormButtonActions::Reset));
ret.add_button(("Cancel".into(), FormButtonActions::Cancel));
ret.push(("Filename".into(), filename.unwrap_or_default().to_string()));
ret.push(("Mime type".into(), mime_type.to_string()));
Some(ret)
}
}
#[derive(Debug)]
pub struct EditAttachmentsRefMut<'a, 'b> {
pub inner: &'a mut EditAttachments,
pub draft: &'b mut Draft,
}
impl std::fmt::Display for EditAttachmentsRefMut<'_, '_> {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "edit attachments")
}
}
impl Component for EditAttachmentsRefMut<'_, '_> {
fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
if let EditAttachmentMode::Edit {
ref mut inner,
no: _,
} = self.inner.mode
{
inner.draw(grid, area, context);
} else if self.is_dirty() {
let attachments_no = self.draft.attachments().len();
let theme_default = crate::conf::value(context, "theme_default");
clear_area(grid, area, theme_default);
if attachments_no == 0 {
write_string_to_grid(
"no attachments",
grid,
theme_default.fg,
theme_default.bg,
theme_default.attrs,
area,
None,
);
} else {
write_string_to_grid(
&format!("{} attachments ", attachments_no),
grid,
theme_default.fg,
theme_default.bg,
theme_default.attrs,
area,
None,
);
for (i, a) in self.draft.attachments().iter().enumerate() {
let bg = if let EditAttachmentCursor::AttachmentNo(u) = self.inner.cursor {
if u == i {
Color::Byte(237)
} else {
theme_default.bg
}
} else {
theme_default.bg
};
if let Some(name) = a.content_type().name() {
write_string_to_grid(
&format!(
"[{}] \"{}\", {} {}",
i,
name,
a.content_type(),
melib::Bytes(a.raw.len())
),
grid,
theme_default.fg,
bg,
theme_default.attrs,
(pos_inc(upper_left!(area), (0, 1 + i)), bottom_right!(area)),
None,
);
} else {
write_string_to_grid(
&format!("[{}] {} {}", i, a.content_type(), melib::Bytes(a.raw.len())),
grid,
theme_default.fg,
bg,
theme_default.attrs,
(pos_inc(upper_left!(area), (0, 1 + i)), bottom_right!(area)),
None,
);
}
}
}
self.inner.buttons.draw(
grid,
(
pos_inc(upper_left!(area), (0, 1 + self.draft.attachments().len())),
bottom_right!(area),
),
context,
);
self.set_dirty(false);
context.dirty_areas.push_back(area);
}
}
fn process_event(&mut self, event: &mut UIEvent, context: &mut Context) -> bool {
if let EditAttachmentMode::Edit {
ref mut inner,
ref no,
} = self.inner.mode
{
if inner.process_event(event, context) {
match inner.buttons_result() {
Some(FormButtonActions::Accept) | Some(FormButtonActions::Cancel) => {
self.inner.mode = EditAttachmentMode::Overview;
}
Some(FormButtonActions::Reset) => {
let no = *no;
if let Some(inner) = self.new_edit_widget(no) {
self.inner.mode = EditAttachmentMode::Edit { inner, no };
}
}
Some(_) | None => {}
}
return true;
}
} else {
match event {
UIEvent::Input(Key::Up) => {
self.set_dirty(true);
match self.inner.cursor {
EditAttachmentCursor::AttachmentNo(ref mut n) => {
if self.draft.attachments().is_empty() {
self.inner.cursor = EditAttachmentCursor::Buttons;
self.inner.buttons.set_focus(true);
self.inner.buttons.process_event(event, context);
return true;
}
*n = n.saturating_sub(1);
}
EditAttachmentCursor::Buttons => {
if !self.inner.buttons.process_event(event, context) {
self.inner.buttons.set_focus(false);
if self.draft.attachments().is_empty() {
return true;
}
self.inner.cursor = EditAttachmentCursor::AttachmentNo(
self.draft.attachments().len() - 1,
);
}
}
}
return true;
}
UIEvent::Input(Key::Down) => {
self.set_dirty(true);
match self.inner.cursor {
EditAttachmentCursor::AttachmentNo(ref mut n) => {
if *n + 1 == self.draft.attachments().len() {
self.inner.cursor = EditAttachmentCursor::Buttons;
self.inner.buttons.set_focus(true);
self.inner.buttons.process_event(event, context);
return true;
}
*n += 1;
}
EditAttachmentCursor::Buttons => {
self.inner.buttons.set_focus(true);
self.inner.buttons.process_event(event, context);
}
}
return true;
}
UIEvent::Input(Key::Char('\n')) => {
match self.inner.cursor {
EditAttachmentCursor::AttachmentNo(ref no) => {
if let Some(inner) = self.new_edit_widget(*no) {
self.inner.mode = EditAttachmentMode::Edit { inner, no: *no };
}
self.set_dirty(true);
}
EditAttachmentCursor::Buttons => {
self.inner.buttons.process_event(event, context);
}
}
return true;
}
_ => {
if self.inner.cursor == EditAttachmentCursor::Buttons
&& self.inner.buttons.process_event(event, context)
{
return true;
}
}
}
}
false
}
fn is_dirty(&self) -> bool {
self.inner.dirty
|| self.inner.buttons.is_dirty()
|| if let EditAttachmentMode::Edit { ref inner, no: _ } = self.inner.mode {
inner.is_dirty()
} else {
false
}
}
fn set_dirty(&mut self, value: bool) {
self.inner.dirty = value;
self.inner.buttons.set_dirty(value);
if let EditAttachmentMode::Edit {
ref mut inner,
no: _,
} = self.inner.mode
{
inner.set_dirty(value);
}
}
fn kill(&mut self, _uuid: Uuid, _context: &mut Context) {}
fn get_shortcuts(&self, _context: &Context) -> ShortcutMaps {
ShortcutMaps::default()
}
fn id(&self) -> ComponentId {
self.inner.id
}
fn set_id(&mut self, new_id: ComponentId) {
self.inner.id = new_id;
}
}

View File

@ -0,0 +1,288 @@
/*
* meli
*
* Copyright 2020 Manos Pitsidianakis
*
* This file is part of meli.
*
* meli is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* meli is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
use super::*;
#[derive(Debug)]
pub enum KeySelection {
LoadingKeys {
handle: JoinHandle<Result<Vec<melib::gpgme::Key>>>,
progress_spinner: ProgressSpinner,
secret: bool,
local: bool,
pattern: String,
allow_remote_lookup: ToggleFlag,
},
Error {
id: ComponentId,
err: MeliError,
},
Loaded {
widget: UIDialog<melib::gpgme::Key>,
keys: Vec<melib::gpgme::Key>,
},
}
impl std::fmt::Display for KeySelection {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "select pgp keys")
}
}
impl KeySelection {
pub fn new(
secret: bool,
local: bool,
pattern: String,
allow_remote_lookup: ToggleFlag,
context: &mut Context,
) -> Result<Self> {
use melib::gpgme::*;
debug!("KeySelection::new");
debug!(&secret);
debug!(&local);
debug!(&pattern);
debug!(&allow_remote_lookup);
let mut ctx = Context::new()?;
if local {
ctx.set_auto_key_locate(LocateKey::LOCAL)?;
} else {
ctx.set_auto_key_locate(LocateKey::WKD | LocateKey::LOCAL)?;
}
let job = ctx.keylist(secret, Some(pattern.clone()))?;
let handle = context.job_executor.spawn_specialized(job);
let mut progress_spinner = ProgressSpinner::new(8, context);
progress_spinner.start();
Ok(KeySelection::LoadingKeys {
handle,
secret,
local,
pattern,
allow_remote_lookup,
progress_spinner,
})
}
}
impl Component for KeySelection {
fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
match self {
KeySelection::LoadingKeys {
ref mut progress_spinner,
..
} => progress_spinner.draw(grid, center_area(area, (2, 2)), context),
KeySelection::Error { ref err, .. } => {
let theme_default = crate::conf::value(context, "theme_default");
write_string_to_grid(
&err.to_string(),
grid,
theme_default.fg,
theme_default.bg,
theme_default.attrs,
center_area(area, (15, 2)),
Some(0),
);
}
KeySelection::Loaded { ref mut widget, .. } => widget.draw(grid, area, context),
}
}
fn process_event(&mut self, event: &mut UIEvent, context: &mut Context) -> bool {
debug!(&self);
debug!(&event);
match self {
KeySelection::LoadingKeys {
ref mut progress_spinner,
ref mut handle,
secret,
local,
ref mut pattern,
allow_remote_lookup,
..
} => match event {
UIEvent::StatusEvent(StatusEvent::JobFinished(ref id)) if *id == handle.job_id => {
match handle.chan.try_recv() {
Err(_) => { /* Job was canceled */ }
Ok(None) => { /* something happened, perhaps a worker thread panicked */ }
Ok(Some(Ok(keys))) => {
if keys.is_empty() {
let id = progress_spinner.id();
if allow_remote_lookup.is_true() {
match Self::new(
*secret,
*local,
std::mem::replace(pattern, String::new()),
*allow_remote_lookup,
context,
) {
Ok(w) => {
*self = w;
}
Err(err) => *self = KeySelection::Error { err, id },
}
} else if !*local && allow_remote_lookup.is_ask() {
*self = KeySelection::Error {
err: MeliError::new(format!(
"No keys found for {}, perform remote lookup?",
pattern
)),
id,
}
} else {
*self = KeySelection::Error {
err: MeliError::new(format!(
"No keys found for {}.",
pattern
)),
id,
}
}
if let KeySelection::Error { ref err, .. } = self {
context.replies.push_back(UIEvent::StatusEvent(
StatusEvent::DisplayMessage(err.to_string()),
));
let res: Option<melib::gpgme::Key> = None;
context
.replies
.push_back(UIEvent::FinishedUIDialog(id, Box::new(res)));
}
return false;
}
let mut widget = UIDialog::new(
"select key",
keys.iter()
.map(|k| {
(
k.clone(),
if let Some(primary_uid) = k.primary_uid() {
format!("{} {}", k.fingerprint(), primary_uid)
} else {
k.fingerprint().to_string()
},
)
})
.collect::<Vec<(melib::gpgme::Key, String)>>(),
true,
Some(Box::new(
move |id: ComponentId, results: &[melib::gpgme::Key]| {
Some(UIEvent::FinishedUIDialog(
id,
Box::new(results.get(0).map(|k| k.clone())),
))
},
)),
context,
);
widget.set_dirty(true);
*self = KeySelection::Loaded { widget, keys };
}
Ok(Some(Err(err))) => {
*self = KeySelection::Error {
err,
id: ComponentId::new_v4(),
};
}
}
false
}
_ => progress_spinner.process_event(event, context),
},
KeySelection::Error { .. } => false,
KeySelection::Loaded { ref mut widget, .. } => widget.process_event(event, context),
}
}
fn is_dirty(&self) -> bool {
match self {
KeySelection::LoadingKeys {
ref progress_spinner,
..
} => progress_spinner.is_dirty(),
KeySelection::Error { .. } => true,
KeySelection::Loaded { ref widget, .. } => widget.is_dirty(),
}
}
fn set_dirty(&mut self, value: bool) {
match self {
KeySelection::LoadingKeys {
ref mut progress_spinner,
..
} => progress_spinner.set_dirty(value),
KeySelection::Error { .. } => {}
KeySelection::Loaded { ref mut widget, .. } => widget.set_dirty(value),
}
}
fn kill(&mut self, _uuid: Uuid, _context: &mut Context) {}
fn get_shortcuts(&self, context: &Context) -> ShortcutMaps {
match self {
KeySelection::LoadingKeys { .. } | KeySelection::Error { .. } => {
ShortcutMaps::default()
}
KeySelection::Loaded { ref widget, .. } => widget.get_shortcuts(context),
}
}
fn id(&self) -> ComponentId {
match self {
KeySelection::LoadingKeys {
ref progress_spinner,
..
} => progress_spinner.id(),
KeySelection::Error { ref id, .. } => *id,
KeySelection::Loaded { ref widget, .. } => widget.id(),
}
}
fn set_id(&mut self, new_id: ComponentId) {
match self {
KeySelection::LoadingKeys {
ref mut progress_spinner,
..
} => progress_spinner.set_id(new_id),
KeySelection::Error { ref mut id, .. } => *id = new_id,
KeySelection::Loaded { ref mut widget, .. } => widget.set_id(new_id),
}
}
}
#[derive(Debug, Clone)]
pub struct GpgComposeState {
pub sign_mail: ToggleFlag,
pub encrypt_mail: ToggleFlag,
pub encrypt_keys: Vec<melib::gpgme::Key>,
pub encrypt_for_self: bool,
pub sign_keys: Vec<melib::gpgme::Key>,
}
impl GpgComposeState {
pub fn new() -> Self {
GpgComposeState {
sign_mail: ToggleFlag::Unset,
encrypt_mail: ToggleFlag::Unset,
encrypt_keys: vec![],
encrypt_for_self: true,
sign_keys: vec![],
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -19,10 +19,9 @@
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
use super::EntryStrings;
use super::*;
use crate::components::utilities::PageMovement;
use crate::jobs::{oneshot, JobId};
use crate::components::PageMovement;
use crate::jobs::JoinHandle;
use std::cmp;
use std::convert::TryInto;
use std::iter::FromIterator;
@ -136,16 +135,8 @@ pub struct CompactListing {
rows_drawn: SegmentTree,
rows: Vec<((usize, (ThreadHash, EnvelopeHash)), EntryStrings)>,
search_job: Option<(
String,
oneshot::Receiver<Result<SmallVec<[EnvelopeHash; 512]>>>,
JobId,
)>,
select_job: Option<(
String,
oneshot::Receiver<Result<SmallVec<[EnvelopeHash; 512]>>>,
JobId,
)>,
search_job: Option<(String, JoinHandle<Result<SmallVec<[EnvelopeHash; 512]>>>)>,
select_job: Option<(String, JoinHandle<Result<SmallVec<[EnvelopeHash; 512]>>>)>,
filter_term: String,
filtered_selection: Vec<ThreadHash>,
filtered_order: HashMap<ThreadHash, usize>,
@ -161,7 +152,7 @@ pub struct CompactListing {
movement: Option<PageMovement>,
modifier_active: bool,
modifier_command: Option<char>,
modifier_command: Option<Modifier>,
id: ComponentId,
}
@ -236,17 +227,10 @@ impl MailListingTrait for CompactListing {
match context.accounts[&self.cursor_pos.0].load(self.cursor_pos.1) {
Ok(()) => {}
Err(_) => {
let default_cell = {
let mut ret = Cell::with_char(' ');
ret.set_fg(self.color_cache.theme_default.fg)
.set_bg(self.color_cache.theme_default.bg)
.set_attrs(self.color_cache.theme_default.attrs);
ret
};
let message: String =
context.accounts[&self.cursor_pos.0][&self.cursor_pos.1].status();
self.data_columns.columns[0] =
CellBuffer::new_with_context(message.len(), 1, default_cell, context);
CellBuffer::new_with_context(message.len(), 1, None, context);
self.length = 0;
write_string_to_grid(
message.as_str(),
@ -401,30 +385,23 @@ impl MailListingTrait for CompactListing {
min_width.0 = self.length.saturating_sub(1).to_string().len();
let default_cell = {
let mut ret = Cell::with_char(' ');
ret.set_fg(self.color_cache.theme_default.fg)
.set_bg(self.color_cache.theme_default.bg)
.set_attrs(self.color_cache.theme_default.attrs);
ret
};
/* index column */
self.data_columns.columns[0] =
CellBuffer::new_with_context(min_width.0, rows.len(), default_cell, context);
CellBuffer::new_with_context(min_width.0, rows.len(), None, context);
/* date column */
self.data_columns.columns[1] =
CellBuffer::new_with_context(min_width.1, rows.len(), default_cell, context);
CellBuffer::new_with_context(min_width.1, rows.len(), None, context);
/* from column */
self.data_columns.columns[2] =
CellBuffer::new_with_context(min_width.2, rows.len(), default_cell, context);
CellBuffer::new_with_context(min_width.2, rows.len(), None, context);
self.data_columns.segment_tree[2] = row_widths.2.into();
/* flags column */
self.data_columns.columns[3] =
CellBuffer::new_with_context(min_width.3, rows.len(), default_cell, context);
CellBuffer::new_with_context(min_width.3, rows.len(), None, context);
/* subject column */
self.data_columns.columns[4] =
CellBuffer::new_with_context(min_width.4, rows.len(), default_cell, context);
CellBuffer::new_with_context(min_width.4, rows.len(), None, context);
self.data_columns.segment_tree[4] = row_widths.4.into();
self.rows = rows;
@ -442,7 +419,7 @@ impl MailListingTrait for CompactListing {
if self.length == 0 && self.filter_term.is_empty() {
let message: String = account[&self.cursor_pos.1].status();
self.data_columns.columns[0] =
CellBuffer::new_with_context(message.len(), self.length + 1, default_cell, context);
CellBuffer::new_with_context(message.len(), self.length + 1, None, context);
write_string_to_grid(
&message,
&mut self.data_columns.columns[0],
@ -564,7 +541,7 @@ impl ListingTrait for CompactListing {
} else if self.new_cursor_pos.2 + rows * multiplier > self.length {
self.new_cursor_pos.2 = self.length - 1;
} else {
self.new_cursor_pos.2 = (self.length / rows) * rows;
self.new_cursor_pos.2 = (self.length.saturating_sub(1) / rows) * rows;
}
}
PageMovement::Right(_) | PageMovement::Left(_) => {}
@ -603,7 +580,9 @@ impl ListingTrait for CompactListing {
self.highlight_line(grid, new_area, *idx, context);
context.dirty_areas.push_back(new_area);
}
return;
if !self.force_draw {
return;
}
} else if self.cursor_pos != self.new_cursor_pos {
self.cursor_pos = self.new_cursor_pos;
}
@ -811,15 +790,8 @@ impl ListingTrait for CompactListing {
self.new_cursor_pos.2 =
std::cmp::min(self.filtered_selection.len() - 1, self.cursor_pos.2);
} else {
let default_cell = {
let mut ret = Cell::with_char(' ');
ret.set_fg(self.color_cache.theme_default.fg)
.set_bg(self.color_cache.theme_default.bg)
.set_attrs(self.color_cache.theme_default.attrs);
ret
};
self.data_columns.columns[0] =
CellBuffer::new_with_context(0, 0, default_cell, context);
CellBuffer::new_with_context(0, 0, None, context);
}
self.redraw_threads_list(
context,
@ -838,15 +810,8 @@ impl ListingTrait for CompactListing {
format!("Failed to search for term {}: {}", &self.filter_term, e),
ERROR,
);
let default_cell = {
let mut ret = Cell::with_char(' ');
ret.set_fg(self.color_cache.theme_default.fg)
.set_bg(self.color_cache.theme_default.bg)
.set_attrs(self.color_cache.theme_default.attrs);
ret
};
self.data_columns.columns[0] =
CellBuffer::new_with_context(message.len(), 1, default_cell, context);
CellBuffer::new_with_context(message.len(), 1, None, context);
write_string_to_grid(
&message,
&mut self.data_columns.columns[0],
@ -860,8 +825,20 @@ impl ListingTrait for CompactListing {
}
}
fn set_command_modifier(&mut self, is_active: bool) {
self.modifier_active = is_active;
fn unfocused(&self) -> bool {
self.unfocused
}
fn set_modifier_active(&mut self, new_val: bool) {
self.modifier_active = new_val;
}
fn set_modifier_command(&mut self, new_val: Option<Modifier>) {
self.modifier_command = new_val;
}
fn modifier_command(&self) -> Option<Modifier> {
self.modifier_command
}
fn set_movement(&mut self, mvm: PageMovement) {
@ -918,9 +895,9 @@ impl CompactListing {
let thread = threads.thread_ref(hash);
let mut tags = String::new();
let mut colors: SmallVec<[_; 8]> = SmallVec::new();
let backend_lck = context.accounts[&self.cursor_pos.0].backend.read().unwrap();
if let Some(t) = backend_lck.tags() {
let tags_lck = t.read().unwrap();
let account = &context.accounts[&self.cursor_pos.0];
if account.backend_capabilities.supports_tags {
let tags_lck = account.collection.tag_index.read().unwrap();
for t in e.labels().iter() {
if mailbox_settings!(
context[self.cursor_pos.0][&self.cursor_pos.1]
@ -1414,8 +1391,7 @@ impl Component for CompactListing {
let (upper_left, bottom_right) = area;
let rows = get_y(bottom_right) - get_y(upper_left) + 1;
if let Some('s') = self.modifier_command.take() {
self.set_command_modifier(false);
if let Some(modifier) = self.modifier_command.take() {
if let Some(mvm) = self.movement.as_ref() {
match mvm {
PageMovement::Up(amount) => {
@ -1423,16 +1399,47 @@ impl Component for CompactListing {
..=self.new_cursor_pos.2
{
let thread = self.get_thread_under_cursor(c);
self.selection.entry(thread).and_modify(|e| *e = !*e);
match modifier {
Modifier::SymmetricDifference => {
self.selection.entry(thread).and_modify(|e| *e = !*e);
}
Modifier::Union => {
self.selection.entry(thread).and_modify(|e| *e = true);
}
Modifier::Difference => {
self.selection.entry(thread).and_modify(|e| *e = false);
}
Modifier::Intersection => {}
}
self.row_updates.push(thread);
}
if modifier == Modifier::Intersection {
for c in (0..self.new_cursor_pos.2.saturating_sub(*amount))
.chain((self.new_cursor_pos.2 + 2)..self.length)
{
let thread = self.get_thread_under_cursor(c);
self.selection.entry(thread).and_modify(|e| *e = false);
self.row_updates.push(thread);
}
}
}
PageMovement::PageUp(multiplier) => {
for c in self.new_cursor_pos.2.saturating_sub(rows * multiplier)
..=self.new_cursor_pos.2
{
let thread = self.get_thread_under_cursor(c);
self.selection.entry(thread).and_modify(|e| *e = !*e);
match modifier {
Modifier::SymmetricDifference => {
self.selection.entry(thread).and_modify(|e| *e = !*e);
}
Modifier::Union => {
self.selection.entry(thread).and_modify(|e| *e = true);
}
Modifier::Difference => {
self.selection.entry(thread).and_modify(|e| *e = false);
}
Modifier::Intersection => {}
}
self.row_updates.push(thread);
}
}
@ -1441,9 +1448,30 @@ impl Component for CompactListing {
..std::cmp::min(self.length, self.new_cursor_pos.2 + amount + 1)
{
let thread = self.get_thread_under_cursor(c);
self.selection.entry(thread).and_modify(|e| *e = !*e);
match modifier {
Modifier::SymmetricDifference => {
self.selection.entry(thread).and_modify(|e| *e = !*e);
}
Modifier::Union => {
self.selection.entry(thread).and_modify(|e| *e = true);
}
Modifier::Difference => {
self.selection.entry(thread).and_modify(|e| *e = false);
}
Modifier::Intersection => {}
}
self.row_updates.push(thread);
}
if modifier == Modifier::Intersection {
for c in (0..self.new_cursor_pos.2).chain(
(std::cmp::min(self.length, self.new_cursor_pos.2 + amount + 1)
+ 1)..self.length,
) {
let thread = self.get_thread_under_cursor(c);
self.selection.entry(thread).and_modify(|e| *e = false);
self.row_updates.push(thread);
}
}
}
PageMovement::PageDown(multiplier) => {
for c in self.new_cursor_pos.2
@ -1453,27 +1481,87 @@ impl Component for CompactListing {
)
{
let thread = self.get_thread_under_cursor(c);
self.selection.entry(thread).and_modify(|e| *e = !*e);
match modifier {
Modifier::SymmetricDifference => {
self.selection.entry(thread).and_modify(|e| *e = !*e);
}
Modifier::Union => {
self.selection.entry(thread).and_modify(|e| *e = true);
}
Modifier::Difference => {
self.selection.entry(thread).and_modify(|e| *e = false);
}
Modifier::Intersection => {}
}
self.row_updates.push(thread);
}
if modifier == Modifier::Intersection {
for c in (0..self.new_cursor_pos.2).chain(
(std::cmp::min(
self.new_cursor_pos.2 + rows * multiplier + 1,
self.length,
) + 1)..self.length,
) {
let thread = self.get_thread_under_cursor(c);
self.selection.entry(thread).and_modify(|e| *e = false);
self.row_updates.push(thread);
}
}
}
PageMovement::Right(_) | PageMovement::Left(_) => {}
PageMovement::Home => {
for c in 0..=self.new_cursor_pos.2 {
let thread = self.get_thread_under_cursor(c);
self.selection.entry(thread).and_modify(|e| *e = !*e);
match modifier {
Modifier::SymmetricDifference => {
self.selection.entry(thread).and_modify(|e| *e = !*e);
}
Modifier::Union => {
self.selection.entry(thread).and_modify(|e| *e = true);
}
Modifier::Difference => {
self.selection.entry(thread).and_modify(|e| *e = false);
}
Modifier::Intersection => {}
}
self.row_updates.push(thread);
}
if modifier == Modifier::Intersection {
for c in (self.new_cursor_pos.2 + 1)..self.length {
let thread = self.get_thread_under_cursor(c);
self.selection.entry(thread).and_modify(|e| *e = false);
self.row_updates.push(thread);
}
}
}
PageMovement::End => {
for c in self.new_cursor_pos.2..self.length {
let thread = self.get_thread_under_cursor(c);
self.selection.entry(thread).and_modify(|e| *e = !*e);
match modifier {
Modifier::SymmetricDifference => {
self.selection.entry(thread).and_modify(|e| *e = !*e);
}
Modifier::Union => {
self.selection.entry(thread).and_modify(|e| *e = true);
}
Modifier::Difference => {
self.selection.entry(thread).and_modify(|e| *e = false);
}
Modifier::Intersection => {}
}
self.row_updates.push(thread);
}
if modifier == Modifier::Intersection {
for c in 0..self.new_cursor_pos.2 {
let thread = self.get_thread_under_cursor(c);
self.selection.entry(thread).and_modify(|e| *e = false);
self.row_updates.push(thread);
}
}
}
}
}
self.force_draw = true;
}
if !self.row_updates.is_empty() {
@ -1539,6 +1627,8 @@ impl Component for CompactListing {
) =>
{
self.unfocused = false;
self.view
.process_event(&mut UIEvent::VisibilityChange(false), context);
self.dirty = true;
/* If self.row_updates is not empty and we exit a thread, the row_update events
* will be performed but the list will not be drawn. So force a draw in any case.
@ -1548,12 +1638,10 @@ impl Component for CompactListing {
}
UIEvent::Input(ref key)
if !self.unfocused
&& shortcut!(
key == shortcuts[CompactListing::DESCRIPTION]["select_entry"]
) =>
&& shortcut!(key == shortcuts[Listing::DESCRIPTION]["select_entry"]) =>
{
if self.modifier_active {
self.modifier_command = Some('s');
if self.modifier_active && self.modifier_command.is_none() {
self.modifier_command = Some(Modifier::default());
} else {
let thread_hash = self.get_thread_under_cursor(self.cursor_pos.2);
self.selection.entry(thread_hash).and_modify(|e| *e = !*e);
@ -1580,7 +1668,7 @@ impl Component for CompactListing {
// FIXME: perform subsort.
return true;
}
Action::ToggleThreadSnooze if !self.unfocused => {
Action::Listing(ToggleThreadSnooze) if !self.unfocused => {
let thread = self.get_thread_under_cursor(self.cursor_pos.2);
let account = &mut context.accounts[&self.cursor_pos.0];
account
@ -1605,6 +1693,43 @@ impl Component for CompactListing {
}
}
match *event {
UIEvent::ConfigReload { old_settings: _ } => {
self.color_cache = ColorCache {
even_unseen: crate::conf::value(context, "mail.listing.compact.even_unseen"),
even_selected: crate::conf::value(
context,
"mail.listing.compact.even_selected",
),
even_highlighted: crate::conf::value(
context,
"mail.listing.compact.even_highlighted",
),
odd_unseen: crate::conf::value(context, "mail.listing.compact.odd_unseen"),
odd_selected: crate::conf::value(context, "mail.listing.compact.odd_selected"),
odd_highlighted: crate::conf::value(
context,
"mail.listing.compact.odd_highlighted",
),
even: crate::conf::value(context, "mail.listing.compact.even"),
odd: crate::conf::value(context, "mail.listing.compact.odd"),
attachment_flag: crate::conf::value(context, "mail.listing.attachment_flag"),
thread_snooze_flag: crate::conf::value(
context,
"mail.listing.thread_snooze_flag",
),
tag_default: crate::conf::value(context, "mail.listing.tag_default"),
theme_default: crate::conf::value(context, "theme_default"),
..self.color_cache
};
if !context.settings.terminal.use_color() {
self.color_cache.highlighted.attrs |= Attr::REVERSE;
self.color_cache.tag_default.attrs |= Attr::REVERSE;
self.color_cache.even_highlighted.attrs |= Attr::REVERSE;
self.color_cache.odd_highlighted.attrs |= Attr::REVERSE;
}
self.refresh_mailbox(context, true);
self.set_dirty(true);
}
UIEvent::MailboxUpdate((ref idxa, ref idxf))
if (*idxa, *idxf) == (self.new_cursor_pos.0, self.cursor_pos.1) =>
{
@ -1698,13 +1823,10 @@ impl Component for CompactListing {
self.cursor_pos.1,
) {
Ok(job) => {
let (chan, handle, job_id) = context.accounts[&self.cursor_pos.0]
let handle = context.accounts[&self.cursor_pos.0]
.job_executor
.spawn_specialized(job);
context.accounts[&self.cursor_pos.0]
.active_jobs
.insert(job_id, crate::conf::accounts::JobRequest::Search(handle));
self.search_job = Some((filter_term.to_string(), chan, job_id));
self.search_job = Some((filter_term.to_string(), handle));
}
Err(err) => {
context.replies.push_back(UIEvent::Notification(
@ -1723,16 +1845,13 @@ impl Component for CompactListing {
self.cursor_pos.1,
) {
Ok(job) => {
let (mut chan, handle, job_id) = context.accounts[&self.cursor_pos.0]
let mut handle = context.accounts[&self.cursor_pos.0]
.job_executor
.spawn_specialized(job);
if let Ok(Some(search_result)) = try_recv_timeout!(&mut chan) {
if let Ok(Some(search_result)) = try_recv_timeout!(&mut handle.chan) {
self.select(search_term, search_result, context);
} else {
context.accounts[&self.cursor_pos.0]
.active_jobs
.insert(job_id, crate::conf::accounts::JobRequest::Search(handle));
self.select_job = Some((search_term.to_string(), chan, job_id));
self.select_job = Some((search_term.to_string(), handle));
}
}
Err(err) => {
@ -1749,24 +1868,30 @@ impl Component for CompactListing {
if self
.search_job
.as_ref()
.map(|(_, _, j)| j == job_id)
.map(|(_, j)| j == job_id)
.unwrap_or(false) =>
{
let (filter_term, mut rcvr, _job_id) = self.search_job.take().unwrap();
let results = rcvr.try_recv().unwrap().unwrap();
self.filter(filter_term, results, context);
let (filter_term, mut handle) = self.search_job.take().unwrap();
match handle.chan.try_recv() {
Err(_) => { /* search was canceled */ }
Ok(None) => { /* something happened, perhaps a worker thread panicked */ }
Ok(Some(results)) => self.filter(filter_term, results, context),
}
self.set_dirty(true);
}
UIEvent::StatusEvent(StatusEvent::JobFinished(ref job_id))
if self
.select_job
.as_ref()
.map(|(_, _, j)| j == job_id)
.map(|(_, j)| j == job_id)
.unwrap_or(false) =>
{
let (search_term, mut rcvr, _job_id) = self.select_job.take().unwrap();
let results = rcvr.try_recv().unwrap().unwrap();
self.select(&search_term, results, context);
let (search_term, mut handle) = self.select_job.take().unwrap();
match handle.chan.try_recv() {
Err(_) => { /* search was canceled */ }
Ok(None) => { /* something happened, perhaps a worker thread panicked */ }
Ok(Some(results)) => self.select(&search_term, results, context),
}
self.set_dirty(true);
}
_ => {}
@ -1797,6 +1922,8 @@ impl Component for CompactListing {
let config_map = context.settings.shortcuts.compact_listing.key_values();
map.insert(CompactListing::DESCRIPTION, config_map);
let config_map = context.settings.shortcuts.listing.key_values();
map.insert(Listing::DESCRIPTION, config_map);
map
}

View File

@ -20,8 +20,8 @@
*/
use super::*;
use crate::components::utilities::PageMovement;
use crate::jobs::{oneshot, JobId};
use crate::components::PageMovement;
use crate::jobs::JoinHandle;
use std::iter::FromIterator;
macro_rules! row_attr {
@ -104,11 +104,7 @@ pub struct ConversationsListing {
/// Cache current view.
content: CellBuffer,
search_job: Option<(
String,
oneshot::Receiver<Result<SmallVec<[EnvelopeHash; 512]>>>,
JobId,
)>,
search_job: Option<(String, JoinHandle<Result<SmallVec<[EnvelopeHash; 512]>>>)>,
filter_term: String,
filtered_selection: Vec<ThreadHash>,
filtered_order: HashMap<ThreadHash, usize>,
@ -124,7 +120,7 @@ pub struct ConversationsListing {
movement: Option<PageMovement>,
modifier_active: bool,
modifier_command: Option<char>,
modifier_command: Option<Modifier>,
id: ComponentId,
}
@ -199,17 +195,9 @@ impl MailListingTrait for ConversationsListing {
match context.accounts[&self.cursor_pos.0].load(self.cursor_pos.1) {
Ok(()) => {}
Err(_) => {
let default_cell = {
let mut ret = Cell::with_char(' ');
ret.set_fg(self.color_cache.theme_default.fg)
.set_bg(self.color_cache.theme_default.bg)
.set_attrs(self.color_cache.theme_default.attrs);
ret
};
let message: String =
context.accounts[&self.cursor_pos.0][&self.cursor_pos.1].status();
self.content =
CellBuffer::new_with_context(message.len(), 1, default_cell, context);
self.content = CellBuffer::new_with_context(message.len(), 1, None, context);
self.length = 0;
write_string_to_grid(
message.as_str(),
@ -299,17 +287,20 @@ impl MailListingTrait for ConversationsListing {
}
from_address_list.clear();
from_address_set.clear();
for (_, h) in threads.thread_group_iter(thread) {
let env_hash = threads.thread_nodes()[&h].message().unwrap();
let envelope: &EnvelopeRef = &context.accounts[&self.cursor_pos.0]
.collection
.get_env(env_hash);
for envelope in threads
.thread_group_iter(thread)
.filter_map(|(_, h)| threads.thread_nodes()[&h].message())
.map(|env_hash| {
context.accounts[&self.cursor_pos.0]
.collection
.get_env(env_hash)
})
{
for addr in envelope.from().iter() {
if from_address_set.contains(addr.raw()) {
if from_address_set.contains(addr.address_spec_raw()) {
continue;
}
from_address_set.insert(addr.raw().to_vec());
from_address_set.insert(addr.address_spec_raw().to_vec());
from_address_list.push(addr.clone());
}
}
@ -357,8 +348,7 @@ impl MailListingTrait for ConversationsListing {
}
let width = max_entry_columns;
self.content =
CellBuffer::new_with_context(width, 4 * rows.len(), Cell::with_char(' '), context);
self.content = CellBuffer::new_with_context(width, 4 * rows.len(), None, context);
let padding_fg = self.color_cache.padding.fg;
@ -488,15 +478,8 @@ impl MailListingTrait for ConversationsListing {
}
}
if self.length == 0 && self.filter_term.is_empty() {
let default_cell = {
let mut ret = Cell::with_char(' ');
ret.set_fg(self.color_cache.theme_default.fg)
.set_bg(self.color_cache.theme_default.bg)
.set_attrs(self.color_cache.theme_default.attrs);
ret
};
let message: String = account[&self.cursor_pos.1].status();
self.content = CellBuffer::new_with_context(message.len(), 1, default_cell, context);
self.content = CellBuffer::new_with_context(message.len(), 1, None, context);
write_string_to_grid(
&message,
&mut self.content,
@ -642,7 +625,7 @@ impl ListingTrait for ConversationsListing {
} else if self.new_cursor_pos.2 + rows * multiplier > self.length {
self.new_cursor_pos.2 = self.length - 1;
} else {
self.new_cursor_pos.2 = (self.length / rows) * rows;
self.new_cursor_pos.2 = (self.length.saturating_sub(1) / rows) * rows;
}
}
PageMovement::Right(_) | PageMovement::Left(_) => {}
@ -650,7 +633,7 @@ impl ListingTrait for ConversationsListing {
self.new_cursor_pos.2 = 0;
}
PageMovement::End => {
self.new_cursor_pos.2 = self.length - 1;
self.new_cursor_pos.2 = self.length.saturating_sub(1);
}
}
}
@ -824,14 +807,7 @@ impl ListingTrait for ConversationsListing {
self.new_cursor_pos.2 =
std::cmp::min(self.filtered_selection.len() - 1, self.cursor_pos.2);
} else {
let default_cell = {
let mut ret = Cell::with_char(' ');
ret.set_fg(self.color_cache.theme_default.fg)
.set_bg(self.color_cache.theme_default.bg)
.set_attrs(self.color_cache.theme_default.attrs);
ret
};
self.content = CellBuffer::new_with_context(0, 0, default_cell, context);
self.content = CellBuffer::new_with_context(0, 0, None, context);
}
self.redraw_threads_list(
context,
@ -850,15 +826,7 @@ impl ListingTrait for ConversationsListing {
format!("Failed to search for term {}: {}", self.filter_term, e),
ERROR,
);
let default_cell = {
let mut ret = Cell::with_char(' ');
ret.set_fg(self.color_cache.theme_default.fg)
.set_bg(self.color_cache.theme_default.bg)
.set_attrs(self.color_cache.theme_default.attrs);
ret
};
self.content =
CellBuffer::new_with_context(message.len(), 1, default_cell, context);
self.content = CellBuffer::new_with_context(message.len(), 1, None, context);
write_string_to_grid(
&message,
&mut self.content,
@ -872,8 +840,20 @@ impl ListingTrait for ConversationsListing {
}
}
fn set_command_modifier(&mut self, is_active: bool) {
self.modifier_active = is_active;
fn unfocused(&self) -> bool {
self.unfocused
}
fn set_modifier_active(&mut self, new_val: bool) {
self.modifier_active = new_val;
}
fn set_modifier_command(&mut self, new_val: Option<Modifier>) {
self.modifier_command = new_val;
}
fn modifier_command(&self) -> Option<Modifier> {
self.modifier_command
}
fn set_movement(&mut self, mvm: PageMovement) {
@ -928,9 +908,9 @@ impl ConversationsListing {
let thread = threads.thread_ref(hash);
let mut tags = String::new();
let mut colors = SmallVec::new();
let backend_lck = context.accounts[&self.cursor_pos.0].backend.read().unwrap();
if let Some(t) = backend_lck.tags() {
let tags_lck = t.read().unwrap();
let account = &context.accounts[&self.cursor_pos.0];
if account.backend_capabilities.supports_tags {
let tags_lck = account.collection.tag_index.read().unwrap();
for t in e.labels().iter() {
if mailbox_settings!(
context[self.cursor_pos.0][&self.cursor_pos.1]
@ -1022,6 +1002,7 @@ impl ConversationsListing {
.as_ref()
.map(String::as_str)
.or(Some("%Y-%m-%d %T")),
false,
),
}
}
@ -1068,17 +1049,20 @@ impl ConversationsListing {
let mut from_address_list = Vec::new();
let mut from_address_set: std::collections::HashSet<Vec<u8>> =
std::collections::HashSet::new();
for (_, h) in threads.thread_group_iter(thread_hash) {
let env_hash = threads.thread_nodes()[&h].message().unwrap();
let envelope: &EnvelopeRef = &context.accounts[&self.cursor_pos.0]
.collection
.get_env(env_hash);
for envelope in threads
.thread_group_iter(thread_hash)
.filter_map(|(_, h)| threads.thread_nodes()[&h].message())
.map(|env_hash| {
context.accounts[&self.cursor_pos.0]
.collection
.get_env(env_hash)
})
{
for addr in envelope.from().iter() {
if from_address_set.contains(addr.raw()) {
if from_address_set.contains(addr.address_spec_raw()) {
continue;
}
from_address_set.insert(addr.raw().to_vec());
from_address_set.insert(addr.address_spec_raw().to_vec());
from_address_list.push(addr.clone());
}
}
@ -1264,8 +1248,7 @@ impl Component for ConversationsListing {
}
let (upper_left, bottom_right) = area;
let rows = (get_y(bottom_right) - get_y(upper_left) + 1) / 3;
if let Some('s') = self.modifier_command.take() {
self.set_command_modifier(false);
if let Some(modifier) = self.modifier_command.take() {
if let Some(mvm) = self.movement.as_ref() {
match mvm {
PageMovement::Up(amount) => {
@ -1273,16 +1256,47 @@ impl Component for ConversationsListing {
..=self.new_cursor_pos.2
{
let thread = self.get_thread_under_cursor(c);
self.selection.entry(thread).and_modify(|e| *e = !*e);
match modifier {
Modifier::SymmetricDifference => {
self.selection.entry(thread).and_modify(|e| *e = !*e);
}
Modifier::Union => {
self.selection.entry(thread).and_modify(|e| *e = true);
}
Modifier::Difference => {
self.selection.entry(thread).and_modify(|e| *e = false);
}
Modifier::Intersection => {}
}
self.row_updates.push(thread);
}
if modifier == Modifier::Intersection {
for c in (0..self.new_cursor_pos.2.saturating_sub(*amount))
.chain((self.new_cursor_pos.2 + 2)..self.length)
{
let thread = self.get_thread_under_cursor(c);
self.selection.entry(thread).and_modify(|e| *e = false);
self.row_updates.push(thread);
}
}
}
PageMovement::PageUp(multiplier) => {
for c in self.new_cursor_pos.2.saturating_sub(rows * multiplier)
..=self.new_cursor_pos.2
{
let thread = self.get_thread_under_cursor(c);
self.selection.entry(thread).and_modify(|e| *e = !*e);
match modifier {
Modifier::SymmetricDifference => {
self.selection.entry(thread).and_modify(|e| *e = !*e);
}
Modifier::Union => {
self.selection.entry(thread).and_modify(|e| *e = true);
}
Modifier::Difference => {
self.selection.entry(thread).and_modify(|e| *e = false);
}
Modifier::Intersection => {}
}
self.row_updates.push(thread);
}
}
@ -1291,9 +1305,30 @@ impl Component for ConversationsListing {
..std::cmp::min(self.length, self.new_cursor_pos.2 + amount + 1)
{
let thread = self.get_thread_under_cursor(c);
self.selection.entry(thread).and_modify(|e| *e = !*e);
match modifier {
Modifier::SymmetricDifference => {
self.selection.entry(thread).and_modify(|e| *e = !*e);
}
Modifier::Union => {
self.selection.entry(thread).and_modify(|e| *e = true);
}
Modifier::Difference => {
self.selection.entry(thread).and_modify(|e| *e = false);
}
Modifier::Intersection => {}
}
self.row_updates.push(thread);
}
if modifier == Modifier::Intersection {
for c in (0..self.new_cursor_pos.2).chain(
(std::cmp::min(self.length, self.new_cursor_pos.2 + amount + 1)
+ 1)..self.length,
) {
let thread = self.get_thread_under_cursor(c);
self.selection.entry(thread).and_modify(|e| *e = false);
self.row_updates.push(thread);
}
}
}
PageMovement::PageDown(multiplier) => {
for c in self.new_cursor_pos.2
@ -1303,24 +1338,83 @@ impl Component for ConversationsListing {
)
{
let thread = self.get_thread_under_cursor(c);
self.selection.entry(thread).and_modify(|e| *e = !*e);
match modifier {
Modifier::SymmetricDifference => {
self.selection.entry(thread).and_modify(|e| *e = !*e);
}
Modifier::Union => {
self.selection.entry(thread).and_modify(|e| *e = true);
}
Modifier::Difference => {
self.selection.entry(thread).and_modify(|e| *e = false);
}
Modifier::Intersection => {}
}
self.row_updates.push(thread);
}
if modifier == Modifier::Intersection {
for c in (0..self.new_cursor_pos.2).chain(
(std::cmp::min(
self.new_cursor_pos.2 + rows * multiplier + 1,
self.length,
) + 1)..self.length,
) {
let thread = self.get_thread_under_cursor(c);
self.selection.entry(thread).and_modify(|e| *e = false);
self.row_updates.push(thread);
}
}
}
PageMovement::Right(_) | PageMovement::Left(_) => {}
PageMovement::Home => {
for c in 0..=self.new_cursor_pos.2 {
let thread = self.get_thread_under_cursor(c);
self.selection.entry(thread).and_modify(|e| *e = !*e);
match modifier {
Modifier::SymmetricDifference => {
self.selection.entry(thread).and_modify(|e| *e = !*e);
}
Modifier::Union => {
self.selection.entry(thread).and_modify(|e| *e = true);
}
Modifier::Difference => {
self.selection.entry(thread).and_modify(|e| *e = false);
}
Modifier::Intersection => {}
}
self.row_updates.push(thread);
}
if modifier == Modifier::Intersection {
for c in (self.new_cursor_pos.2 + 1)..self.length {
let thread = self.get_thread_under_cursor(c);
self.selection.entry(thread).and_modify(|e| *e = false);
self.row_updates.push(thread);
}
}
}
PageMovement::End => {
for c in self.new_cursor_pos.2..self.length {
let thread = self.get_thread_under_cursor(c);
self.selection.entry(thread).and_modify(|e| *e = !*e);
match modifier {
Modifier::SymmetricDifference => {
self.selection.entry(thread).and_modify(|e| *e = !*e);
}
Modifier::Union => {
self.selection.entry(thread).and_modify(|e| *e = true);
}
Modifier::Difference => {
self.selection.entry(thread).and_modify(|e| *e = false);
}
Modifier::Intersection => {}
}
self.row_updates.push(thread);
}
if modifier == Modifier::Intersection {
for c in 0..self.new_cursor_pos.2 {
let thread = self.get_thread_under_cursor(c);
self.selection.entry(thread).and_modify(|e| *e = false);
self.row_updates.push(thread);
}
}
}
}
}
@ -1397,6 +1491,8 @@ impl Component for ConversationsListing {
) =>
{
self.unfocused = false;
self.view
.process_event(&mut UIEvent::VisibilityChange(false), context);
self.dirty = true;
/* If self.row_updates is not empty and we exit a thread, the row_update events
* will be performed but the list will not be drawn. So force a draw in any case.
@ -1406,12 +1502,10 @@ impl Component for ConversationsListing {
}
UIEvent::Input(ref key)
if !self.unfocused
&& shortcut!(
key == shortcuts[ConversationsListing::DESCRIPTION]["select_entry"]
) =>
&& shortcut!(key == shortcuts[Listing::DESCRIPTION]["select_entry"]) =>
{
if self.modifier_active {
self.modifier_command = Some('s');
if self.modifier_active && self.modifier_command.is_none() {
self.modifier_command = Some(Modifier::default());
} else {
let thread_hash = self.get_thread_under_cursor(self.cursor_pos.2);
self.selection.entry(thread_hash).and_modify(|e| *e = !*e);
@ -1508,7 +1602,7 @@ impl Component for ConversationsListing {
*/
return true;
}
Action::ToggleThreadSnooze if !self.unfocused => {
Action::Listing(ToggleThreadSnooze) if !self.unfocused => {
let thread = self.get_thread_under_cursor(self.cursor_pos.2);
let account = &mut context.accounts[&self.cursor_pos.0];
account
@ -1531,6 +1625,34 @@ impl Component for ConversationsListing {
}
}
match *event {
UIEvent::ConfigReload { old_settings: _ } => {
self.color_cache = ColorCache {
theme_default: crate::conf::value(context, "mail.listing.conversations"),
subject: crate::conf::value(context, "mail.listing.conversations.subject"),
from: crate::conf::value(context, "mail.listing.conversations.from"),
date: crate::conf::value(context, "mail.listing.conversations.date"),
selected: crate::conf::value(context, "mail.listing.conversations.selected"),
unseen: crate::conf::value(context, "mail.listing.conversations.unseen"),
highlighted: crate::conf::value(
context,
"mail.listing.conversations.highlighted",
),
attachment_flag: crate::conf::value(context, "mail.listing.attachment_flag"),
thread_snooze_flag: crate::conf::value(
context,
"mail.listing.thread_snooze_flag",
),
tag_default: crate::conf::value(context, "mail.listing.tag_default"),
..self.color_cache
};
if !context.settings.terminal.use_color() {
self.color_cache.highlighted.attrs |= Attr::REVERSE;
self.color_cache.tag_default.attrs |= Attr::REVERSE;
}
self.refresh_mailbox(context, true);
self.set_dirty(true);
}
UIEvent::MailboxUpdate((ref idxa, ref idxf))
if (*idxa, *idxf) == (self.new_cursor_pos.0, self.cursor_pos.1) =>
{
@ -1555,13 +1677,10 @@ impl Component for ConversationsListing {
self.cursor_pos.1,
) {
Ok(job) => {
let (chan, handle, job_id) = context.accounts[&self.cursor_pos.0]
let handle = context.accounts[&self.cursor_pos.0]
.job_executor
.spawn_specialized(job);
context.accounts[&self.cursor_pos.0]
.active_jobs
.insert(job_id, crate::conf::accounts::JobRequest::Search(handle));
self.search_job = Some((filter_term.to_string(), chan, job_id));
self.search_job = Some((filter_term.to_string(), handle));
}
Err(err) => {
context.replies.push_back(UIEvent::Notification(
@ -1601,12 +1720,15 @@ impl Component for ConversationsListing {
if self
.search_job
.as_ref()
.map(|(_, _, j)| j == job_id)
.map(|(_, j)| j == job_id)
.unwrap_or(false) =>
{
let (filter_term, mut rcvr, _job_id) = self.search_job.take().unwrap();
let results = rcvr.try_recv().unwrap().unwrap();
self.filter(filter_term, results, context);
let (filter_term, mut handle) = self.search_job.take().unwrap();
match handle.chan.try_recv() {
Err(_) => { /* search was canceled */ }
Ok(None) => { /* something happened, perhaps a worker thread panicked */ }
Ok(Some(results)) => self.filter(filter_term, results, context),
}
self.set_dirty(true);
}
_ => {}
@ -1638,6 +1760,8 @@ impl Component for ConversationsListing {
let config_map = context.settings.shortcuts.compact_listing.key_values();
map.insert(ConversationsListing::DESCRIPTION, config_map);
let config_map = context.settings.shortcuts.listing.key_values();
map.insert(Listing::DESCRIPTION, config_map);
map
}

View File

@ -20,7 +20,7 @@
*/
use super::*;
use crate::components::utilities::PageMovement;
use crate::components::PageMovement;
#[derive(Debug)]
pub struct OfflineListing {
@ -79,6 +79,10 @@ impl ListingTrait for OfflineListing {
fn draw_list(&mut self, _: &mut CellBuffer, _: Area, _: &mut Context) {}
fn unfocused(&self) -> bool {
false
}
fn set_movement(&mut self, _: PageMovement) {}
}

Some files were not shown because too many files have changed in this diff Show More