Compare commits

..

675 commits

Author SHA1 Message Date
the-djmaze
51de013007 Merge branch 'master' into php81 2026-10-23 13:56:32 +02:00
the-djmaze
a0e093d527 Several security advisories for NPM packages break the JS compiler.
Don't run `npm audit fix`!!!
2026-10-23 12:58:16 +02:00
the-djmaze
19de853729 PHP 8.5 has opcache built in 2026-10-23 11:17:43 +02:00
the-djmaze
e1e94c815d The backtick (`) operator is deprecated, use shell_exec() instead 2026-10-22 23:02:00 +02:00
the-djmaze
c9f7d27551 Proxy requires mainaccount 2026-10-22 22:29:45 +02:00
the-djmaze
843b69bfa8 Send Accept-Encoding header in integrity test? 2026-10-22 22:29:04 +02:00
the-djmaze
61ebcb6ed4 CSP block legacy HTML element types such as <object>, <embed>, and <applet> on Raw actions 2026-10-22 18:04:40 +02:00
Maarten
ceb9f68ded
Merge pull request #1922 from leojonathanoh/fix/docker-ensure-nginx-does-not-listen-on-ipv6-in-ipv4-only-environments
Fix (docker): Ensure nginx does not listen on ipv6 in ipv4-only environments
2026-10-22 16:11:58 +02:00
Maarten
d7e7250b16
Merge pull request #2060 from FathiBenNasr/fix/carddav-sync-empty-set-guard
AddressBook: do not let an empty side wipe the other during sync
2026-10-22 16:09:56 +02:00
Maarten
0a8e37ccc8
Merge pull request #2070 from FathiBenNasr/feature/ldap-suggestions-multiple-base-dns
ldap-contacts-suggestions: search several base DNs
2026-10-22 16:07:51 +02:00
Maarten
2cff632b2d
Merge pull request #2021 from kristofer84/login-o365-bugfix
Corrected OAuth2 login flow and added better error handling
2026-10-22 14:15:28 +02:00
the-djmaze
2c658b7c31 Fix year for #1882 2026-10-22 14:12:26 +02:00
Maarten
4997853b78
Merge pull request #1882 from ZephOne/also-use-ldap-login-mapping-for-smtp-user
Also use ldap login mapping for smtp user
2026-10-22 14:10:42 +02:00
Maarten
6fba843d15
Merge pull request #2011 from juschleg/patch-1
Update SSLContext.php: fixed typo in line 42, to reference correct class
2026-10-22 14:06:53 +02:00
Maarten
3659552fac
Merge pull request #2035 from alterative-editions/plugin-login-http
Add HTTP based SSO plugin for Apache using PHP_AUTH_USER/PHP_AUTH_PW
2026-10-22 13:54:05 +02:00
the-djmaze
3432795930 Fix formatting and bump version for #2019 2026-10-22 13:50:36 +02:00
Maarten
fb4a247916
Merge pull request #2019 from oGrzegorZo/fix-search-filters-keyword-crash
Fix Search Filters plugin: crash on keyword filters
2026-10-22 13:44:56 +02:00
Maarten
a174bf7ba0
Merge pull request #1879 from tribut/smime-intermediate-certs
Add intermediate certificates for S/MIME signing
2026-10-22 13:41:20 +02:00
Maarten
77be04d427
Merge pull request #1863 from SergeyMosin/pgp-improvements
PGP improvements
2026-10-22 13:39:45 +02:00
Maarten
cf185b9ab5
Merge pull request #2066 from Ron-RONZZ-org/feature/admin-trusted-header-auth
Admin panel: accept trusted proxy-auth header (X-NC-Admin) on the admin host
2026-10-22 13:37:52 +02:00
Maarten
30bb5c934d
Merge pull request #2069 from SebastianAlvarezGajardo/fix/messagelist-keep-page-and-search
Don't reset the message list to page 1 and clear the search on message.show
2026-10-22 13:23:57 +02:00
Maarten
b9397e6e20
Merge pull request #2007 from Murena-SAS/dev/fix-autologin
Fix(auth): Correct OIDC login by using SensitiveString object.
2026-10-22 13:18:56 +02:00
the-djmaze
fd6beedc0c Merge branch 'master' of github.com:the-djmaze/snappymail 2026-10-22 13:16:11 +02:00
the-djmaze
45b94153c1 Resolve #1906 2026-10-22 13:15:59 +02:00
Maarten
02e96a9339
Merge pull request #2012 from juschleg/patch-2
fixed typo in imapsync.php: was "RainLoop\API" instead of "RainLoop\Api"
2026-10-22 13:12:04 +02:00
the-djmaze
b3edb5fb0a Versionnumber for #2064 2026-10-22 13:10:09 +02:00
Maarten
58d553b1a2
Merge pull request #2064 from FathiBenNasr/fix/ics-viewer-attachments-observable
ics-viewer: unwrap attachments and widen the calendar part match
2026-10-22 13:07:14 +02:00
Maarten
2f26779f4a
Merge pull request #2061 from FathiBenNasr/feature/avatars-contact-photo
avatars: use the PHOTO stored on a matching contact
2026-10-22 13:01:15 +02:00
Maarten
9f0e6d946a
Merge pull request #2074 from FathiBenNasr/fix/getcontactbyemail-missing-columns
PdoAddressBook: GetContactByEmail() passed the wrong columns
2026-10-22 12:59:25 +02:00
Maarten
85b142f559
Merge pull request #2039 from gocowim/fix_autocrypt_compliance
FIX non-compliant Autocrypt Header
2026-10-22 12:50:26 +02:00
the-djmaze
1be5abd122 Version number for #2024 2026-10-22 12:48:32 +02:00
Maarten
a9ca71bae1
Merge pull request #2024 from bohwaz/fix-remote-login
Fix login-remote plugin password handling
2026-10-22 12:45:17 +02:00
Maarten
0805f71461
Merge pull request #2005 from LoredCast/patch-1
Update PostgreSQL DSN in Docker.md
2026-10-22 12:44:25 +02:00
Maarten
abb83e48b0
Merge pull request #1967 from PhilippMundhenk/master
Plugin proxy-auth: Update instructions for Dovecot 2.4, "check proxy" setting fix, troubleshooting hints
2026-10-22 12:36:44 +02:00
Maarten
f52249b77b
Merge pull request #1973 from HeySora/add-sqlite-to-docker
Add pdo_sqlite installation to Docker image
2026-10-22 12:32:48 +02:00
Maarten
1bbd7f635a
Merge pull request #2052 from alexgabi/patch-1
Update user.json
2026-10-22 12:30:13 +02:00
Maarten
8a8304e81a
Merge pull request #2075 from FathiBenNasr/fix/htmlutils-data-attributes
HtmlUtils: strip every data-* attribute, not just the first one
2026-10-22 12:28:44 +02:00
Maarten
e7a491b0f5
Merge pull request #2079 from MiDniGG/fix/autosave-duplicate-contacts
Fix duplicate contacts on autosave for addresses with uppercase letters
2026-10-22 12:18:12 +02:00
the-djmaze
6c9d3edc18 Docker upgrade to PHP 8.5 #2086 2026-10-22 12:13:49 +02:00
Kristofer Nilsson
5dca6d4f65 Change viewModelTemplateID check for account popups 2026-10-22 12:12:51 +02:00
Cursor Agent
2af0af173e Refactor O365 login to use server-side state and add account support
Co-authored-by: kristofer.john.nilsson <kristofer.john.nilsson@gmail.com>
2026-10-22 12:12:51 +02:00
Kristofer Nilsson
7fdc77ce5f Corrected OAuth2 login flow and added better error handling 2026-10-22 12:12:51 +02:00
Maarten
86dc155d1c
Merge branch 'master' into patch-1 2026-10-21 15:44:30 +02:00
Maarten
5de44c77c2
Merge pull request #2084 from sasogu/add-catalan-localization
Add Catalan localization
2026-10-21 15:42:25 +02:00
the-djmaze
dfc9420385 Sabre XML v4.1.0 2026-10-21 15:18:43 +02:00
the-djmaze
e66f325aa3 Sabre VObject v5.0.0 2026-10-21 15:18:17 +02:00
the-djmaze
aad651f2a1 Remove buggy Squire ImageResizer 2026-10-20 23:41:06 +02:00
the-djmaze
67a68915db Knockout use Reflection binding 2026-10-20 23:34:48 +02:00
the-djmaze
71287022ca Make Languages modal 10px wider 2026-10-20 23:33:40 +02:00
the-djmaze
3da27c3bc8 window.localStorage to localStorage 2026-10-20 23:33:07 +02:00
the-djmaze
020927cc56 Squire v2.4.9 2026-10-20 23:32:32 +02:00
the-djmaze
8667af700d Improved font-size handling to prevent issue #1082 2026-10-20 16:25:00 +02:00
Samuel Soriano
4e65e7ee84 Use the username label for login fields 2026-10-16 17:54:39 +02:00
Samuel Soriano
aca14765c9 Review Catalan terminology, grammar and action meanings 2026-10-16 17:53:07 +02:00
Samuel Soriano
be91e4eb80 Document the Catalan language selector preview 2026-10-16 16:20:42 +02:00
Samuel Soriano
24d7dee9b9 Add Catalan localization 2026-10-16 16:19:20 +02:00
Andreas Pries
6ef8a9a391 Fix duplicate contacts on autosave for addresses with uppercase letters
IncFrec() lowercases the existing addresses but compares them against the
new address in its original case, so an address with uppercase letters is
never found and a new contact is created on every send.

Fixes #1767
2026-09-29 22:48:07 +02:00
Fathi Ben Nasr
08ddc323f9 HtmlUtils: strip every data-* attribute, not just the first one
BuildHtml() iterates $oElement->attributes -- a live DOMNamedNodeMap --
and calls removeAttribute() on the attribute the foreach is currently on.
That ends the iteration: the loop sees no further attribute, so exactly
one data-* is removed per element however many it carries.

Measured on PHP 8.5.10 / libxml 2.12.5, data-* left behind:

  <p data-a>                          -> none
  <p data-a data-b>                   -> data-b
  <p data-a data-b data-c>            -> data-b, data-c
  <p data-a data-b data-c data-d>     -> data-b, data-c, data-d
  <p DATA-A data-b>                   -> data-b

The single call site is buildMessage() in RainLoop/Actions/Messages.php,
reached from both DoSaveMessage() and DoSendMessage(), as the docblock on
BuildHtml() says. So the attributes that survive go out in the sent
message and into the draft that is APPENDed and read back later. It is
not the display path: an incoming body is only decoded and charset
converted in MailSo\Mail\Message::fromFetchResponse(), and the browser
filters it with the allowlists in dev/Common/Html.js.

The fix collects the names first, then removes them.
2026-09-05 21:44:57 +00:00
Fathi Ben Nasr
279cc6406e PdoAddressBook: GetContactByEmail() passed the wrong columns
GetContactByEmail() runs

    SELECT DISTINCT id_contact FROM rainloop_ab_properties WHERE ...

and hands the statement to getContactsFromPDO(), which unconditionally reads
id_contact_str, changed, etag and jcard from every row. None of those are
selected, so every match raises three PHP warnings:

    Undefined array key "id_contact_str"
    Undefined array key "changed"
    Undefined array key "etag"

and the Contact that comes back carries IdContactStr '' with Changed and Etag
both 0 -- values a CardDAV sync would use.

The lookup itself still works: with no jcard column either, every row falls
into the follow-up properties query, so the contact data is right and only the
metadata is wrong. That is what makes it easy to miss.

It is not rare. The avatars plugin calls GetContactByEmail() for the sender of
a message, so a mailbox view triggers it repeatedly -- 23 warnings in a single
session on one of our installs.

Fix: look the contact up by id instead. GetContactByID() already selects the
columns getContactsFromPDO() expects, so the Contact comes back complete, and
the search query keeps doing what it is good at -- finding the id. The unused
$oContact and $iIdContact locals go with it.

Verified against a real PDO: the current query returns one column and the
reader warns on id_contact_str; the replacement returns all of them, warns
nothing, and yields the real IdContactStr and changed instead of '' and 0.

Note: this layer has no test suite upstream, so the verification is a
standalone reproduction rather than an added test.
2026-09-05 03:37:09 +00:00
Fathi Ben Nasr
3dd6c85c87 ldap-contacts-suggestions: search several base DNs
A directory rarely keeps everything worth suggesting in one branch: people
sit in a people branch, meeting rooms and other bookable resources in a
separate one. Searching a single subtree misses them, and widening the base
to the domain root drags every service and role account into the list.

base_dn now accepts several branches separated by '|', which cannot appear
unescaped in a DN — an existing single-branch value parses to a one-element
list and behaves exactly as before. A branch that fails to answer is logged
with its own DN and no longer discards the whole lookup.
2026-08-24 01:20:24 +00:00
SebaSalvaG
e4ecda0e61 Don't reset the message list to page 1 and clear the search on message.show
The `mailbox.message.show` handler rewrote the hash to `mailBox(sFolder)` --
page 1, no search -- even when the user was already in that folder.

Because the hash of an open message carries the message id and the new one does
not, the two differ, the route fires again, the message is shown again, and the
handler runs again: a loop. Every iteration aborts whatever message list request
was pending, so browsing to page 2 or running a search silently bounces back to
page 1 of the INBOX with an empty search box.

Only navigate when we are not already in that folder. Clicking a new-mail
notification from another folder still takes you to the INBOX; if the message is
not in the current page, the else branch already opens it in the preview pane
without touching the list.

The same unconditional setHash a few lines below, for non-INBOX folders, gets
the same guard.
2026-08-23 22:38:40 +02:00
Rong Zhou
640c567210 Admin panel: accept trusted proxy-auth header (X-NC-Admin) on the admin host
Allows gating the SnappyMail admin panel behind an external single sign-on
layer (e.g. a reverse-proxy auth_request validated against an OpenID Connect /
Nextcloud session) without embedding an OIDC client in SnappyMail. The header
is honored only when present AND the request Host matches admin_panel.host
(defense in depth), so it has no effect on regular webmail or on the panel
when admin_panel.host is unset.

Behavior is unchanged when the header is absent: the existing
admin_login/admin_password (+ optional admin_totp) check applies.

Contributed from Ron-RONZZ-org/ronzz-nextcloud (webmail/patches/).
2026-08-16 18:17:08 +02:00
Fathi Ben Nasr
8a2d8e86a5 ics-viewer: unwrap attachments and widen the calendar part match
The panel never appears on a real invitation.

msg.attachments is a ko.observableArray, so msg.attachments.find() is
undefined and throws inside the message subscription - the rest of the
handler, including the JSON-LD path, dies with it. The app itself always
writes attachments().

A scheduling mail also does not necessarily carry a text/calendar
attachment. Evolution and Exchange put the scheduling part inline in
multipart/alternative as text/calendar and repeat it as an
application/ics attachment, so such a message shows two .ics files and
the current test matches neither reliably. Match text/calendar,
application/ics, text/x-vcalendar or an .ics file name - which is what
the commented-out line above was reaching for.
2026-08-14 10:12:06 +00:00
Fathi Ben Nasr
6a7cbe070c avatars: find the photo when several contacts share an address 2026-08-13 02:19:03 +00:00
Fathi Ben Nasr
1a0c9872b2 AddressBook: do not let an empty side wipe the other during sync
Sync() treats an empty list on either side as authoritative:

* a local contact holding an etag but missing from the remote listing is
  deleted locally, so an empty or unparsed listing removes every
  previously synced contact;
* a local tombstone is deleted remotely, so a local store that is empty
  apart from tombstones prunes the server.

An empty list is far more often a fault than a real "delete everything":
a fresh or rebuilt local store, a listing that failed to parse, a stale
server-side index. On a server whose DAV index had lost its records the
listing came back empty and the first sync removed 7 local contacts that
existed on both sides moments earlier.

Skip the deletions in whichever direction the source list is empty and
let the existing import/export reconcile instead. Both guards log a
warning with the counts, so a skipped pass is visible rather than silent.
When both sides hold data the behaviour is unchanged.
2026-08-12 23:38:53 +00:00
Philipp Mundhenk
863f719da3 proxy-auth: remove check_proxy / proxy_ip settings
The "Check Proxy" option inspected the forwarded client IP returned
by Http::GetClientIp(true), which resolves to HTTP_X_FORWARDED_FOR
first and only falls back to REMOTE_ADDR. In any typical reverse-proxy
deployment that header carries the end-user's IP, not the proxy's, so
the option never actually validated that the request came from the
trusted proxy. It only constrained the set of end-user IPs allowed
to use SSO, which is not a meaningful guarantee on its own.

Drop the option, the paired proxy_ip subnet field, and the
ip_in_range helper. Document that access must be gated at the
network layer (firewall / docker network / bound interface) and
that the upstream proxy must strip any client-supplied value of
the configured remote-user header.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 10:44:36 +02:00
Philipp Mundhenk
68588287ef proxy-auth: document APP_SALT rotation troubleshooting
Add README note explaining that AUTHENTICATIONFAILED after a
container rebuild or data reset is typically caused by an
APP_SALT change invalidating the encrypted master credentials,
and that re-entering them in the admin UI restores access.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 09:57:22 +02:00
Philipp Mundhenk
60ee74c68a proxy-auth: fix check_proxy enforcement and persistence
The proxy IP check was never enforced: ServiceProxyAuth read config
key 'proxy_check' but the property is registered as 'check_proxy',
so the lookup always returned the default empty string and the
else-branch set $sProxyRequest = true unconditionally.

The BOOL property also had SetEncrypted(), which caused getDecrypted()
to call DecryptFromJSON() on a literal bool and fail with
"DecryptFromJSON() invalid $data", so the admin UI never reflected
the saved value. BOOLs are stored as plain bools in JSON; switch to
Get() and drop SetEncrypted() on the property.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 09:54:54 +02:00
Philipp Mundhenk
ed35c8e91e
Merge branch 'the-djmaze:master' into master 2026-05-17 09:47:52 +02:00
alexgabi
f757d58534
Update user.json
Rewieved and completed basque translation
2026-05-04 13:11:34 +02:00
the-djmaze
c154d23cfe Ommit Nextcloud "max-version" as it is optional 2026-03-11 17:56:44 +01:00
Till Wimmer
c1f1faca58 FIX non-compliant Autocrypt Header 2026-02-13 02:06:33 +01:00
Super User
dc0191aaf1 Add HTTP based SSO plugin for Apache using PHP_AUTH_USER/PHP_AUTH_PW 2026-02-09 19:47:30 +00:00
bohwaz
a8117a9d03 Fix login-remote plugin password handling 2026-01-06 00:04:41 +01:00
oGrzegorZo
f2648a54c0
Fix Search Filters plugin: crash on keyword filters
Fix Search Filters plugin: crash on keyword filters

- Added try/catch in searchMessages() to prevent fatal IMAP errors.  
- Correctly handle keyword filters ($label1, $label2) with multi-folder search.  
- Extracted applyActions() to handle fSeen/fFlag/fFolder consistently.  
- Ensures plugin no longer crashes when messages exist with a tag.
2025-12-16 14:19:30 +01:00
Jürgen Schlegelmilch
1dc26f11d2 fixed typo in imapsync.php: was "RainLoop\API" instead of "RainLoop\Api" 2025-11-19 23:10:15 +01:00
juschleg
c203d15b17
Update SSLContext.php: fixed typo in line 42, to reference correct class
The reference to class RainLoop\API is invalid, the author obviously meant class RainLoop\Api
2025-11-15 23:12:25 +01:00
Avinash Gusain
2c54ac9be3 Fix(auth): Correct OIDC login by using SensitiveString object. 2025-11-12 14:41:47 +05:30
Manuel
2c73a955e2
Update PostgreSQL DSN in Docker.md
docker compose file defines network as db not postgres
2025-11-09 20:00:39 +01:00
HeySora
658b97f5b4
Add pdo_sqlite installation to Docker image 2025-07-16 12:50:18 +02:00
the-djmaze
ba496919dd Catch Throwable for #1914 2025-06-16 17:35:11 +02:00
Maarten
24bb509b96
Merge pull request #1914 from sgoranov/fix/1913-datetime-overflow
Fix DateTimeHelper::ParseRFC2822DateString integer overflow on 32-bit PHP
2025-06-16 17:30:55 +02:00
Maarten
56409dcbb4
Merge branch 'master' into pgp-improvements 2025-06-16 17:28:39 +02:00
Maarten
b4a6068d5b
Merge pull request #1875 from bapt/fr
fr: add an space before the colon
2025-06-16 17:26:21 +02:00
Maarten
40cf2b950e
Merge pull request #1931 from TLTDevRu/fix-ru-lang
Fix ru lang
2025-06-16 17:23:27 +02:00
Maarten
7397a95a7c
Merge pull request #1941 from bober757/master
incorrect using of the preposition
2025-06-16 17:22:21 +02:00
the-djmaze
9dd7c021fd Small change for #1948 to make it relative to font size 2025-06-16 17:21:26 +02:00
Maarten
9f6d2e4cb7
Merge pull request #1948 from makoehr/compact-folder-list-for-snappymail
More compact view of folder list
2025-06-16 17:19:20 +02:00
Maarten
93e3c431ec
Merge pull request #1961 from Psychi1/Cp1252
Fix the Cp1252 encoding problem
2025-06-16 17:03:26 +02:00
Philipp Mundhenk
a9b89fd7b3
Update instructions for Dovecot 2.4 2025-06-07 13:06:23 +02:00
Psychi
da96945f39
fix regex match-group 2025-05-25 21:13:55 +02:00
Psychi
0bc9950b42
Fix Cp1252 encoding 2025-05-25 21:04:40 +02:00
Mathias Koehrer
c3660bee57 More compact view of folder list
With many folders, the line-height is too large.
It is reduced to make the view more compact.
Also, the top margin for the number of unread-folder will
be adapted.
2025-04-11 17:19:19 +02:00
the-djmaze
dea7f4d1d8 openssl_pkey_free is deprecated 2025-04-04 23:52:27 +02:00
6o6ep
00128e837f
incorrect using of the preposition 2025-04-02 10:22:23 +03:00
6o6ep
d9eaad24ad
incorrect using of the preposition 2025-04-02 10:21:05 +03:00
Roman Chutchev
9565559213
Update user.json 2025-03-11 23:10:42 +02:00
Roman Chutchev
1c9c9f0d6b
Merge branch 'the-djmaze:master' into fix-ru-lang 2025-03-11 22:37:35 +02:00
Maarten
d3b120bbe7
Merge pull request #1880 from tribut/i18n-de-fixes
Fix incorrect German translation
2025-03-10 16:45:38 +01:00
Maarten
a024d34f6d
Merge pull request #1891 from HeySora/master
Automatically add favicon host to CSP if present
2025-03-10 16:43:35 +01:00
the-djmaze
33fbaa9598 Bump version for #1918 2025-03-10 16:35:27 +01:00
Maarten
86da00eb50
Merge pull request #1918 from TomsProject/master
Changing Gravatar hashing algorithm to sha256
2025-03-10 16:34:17 +01:00
the-djmaze
45c8f71af0 Resolve #1883 2025-03-10 16:14:33 +01:00
the-djmaze
70aebb4981 Resolve #1930 2025-03-10 15:48:11 +01:00
Roman Chutchev
0233c83719
Update user.json 2025-03-08 20:51:14 +02:00
Leonard Jonathan Oh
e37546b795 Fix (docker): Ensure nginx does not listen on ipv6 in ipv4-only environments
The official nginx images do not listen on ipv6, very likely to avoid nginx crashing on startup in ipv4-only environments:

```
docker run --rm -it nginx:alpine cat /etc/nginx/conf.d/default.conf | grep -i listen
```

Hence, we simply need to detect whether ipv6 is disabled, and not listen on ipv6 if ipv6 is disabled in the kernel or in the container.
2025-02-26 06:25:09 +00:00
TomsProject
2741762199
Changing hashing algorithm to sha256 2025-02-22 18:32:21 +08:00
Simeon Goranov
ecbd13f099 Fix DateTimeHelper::ParseRFC2822DateString integer overflow on 32-bit PHP 2025-02-12 16:10:47 +02:00
HeySora
5bba3dac82
Automatically add favicon host to CSP if present 2025-01-14 13:27:26 +01:00
ZephOne
ed4b2dd4e1
Also map sSmtpUser to ldap login in plugin ldap-login-mapping 2025-01-09 16:30:26 +01:00
ZephOne
fad9d5e482
Explicit ldap-login-mapping plugin description 2025-01-09 16:28:12 +01:00
Felix Eckhofer
1660c62aa3
Fix incorrect German translation 2025-01-08 13:36:48 +01:00
Felix Eckhofer
dddf360db6
Add intermediate certificates for S/MIME signing 2025-01-07 21:32:26 +01:00
Baptiste Daroussin
7268dafffd fr: add an space before the colon
When composing a message in reply to an email, a colon is appended to
the message which is translated, in french colon should always be
prepended by an unbreakable space.

Add a space in the translation sentence to make sure the space appears
before the colon in the interface
2024-12-27 09:19:08 +01:00
the-djmaze
8bf4727555 Also detect Identity using from address #1865 2024-12-10 14:51:55 +01:00
Maarten
208b1fab62
Merge pull request #1857 from lenny87/cs-translation
Finished czech translation of Snappymail
2024-12-10 14:33:02 +01:00
Maarten
149cf5a120
Merge pull request #1861 from hungyao/patch-1
Update Html.js to remove x-html tags before rendering
2024-12-10 14:13:27 +01:00
Sergey Mosin
5973d71e2f Add an option to store(and delete) encrypted private key passphrase in the browser across multiple sessions 2024-12-09 11:18:41 -05:00
Sergey Mosin
486acde2e3 fix: properly convert minutes to milliseconds for debounce function Passphrases.handle 2024-12-08 19:52:49 -05:00
Andrew Tjia
e16d0b52ae
Update Html.js to remove x-html tags before rendering
Fix for https://github.com/the-djmaze/snappymail/issues/1860 to remove <x-html></x-html> tags that might be emitted by certain email clients.

Tested by modifying locally using JS debugger and Snappymail JS debug mode.
2024-12-07 09:37:33 +09:00
Sergey Mosin
c0348893af Delete OpenPGP keys on the server when they are deleted in the browser 2024-12-05 11:14:03 -05:00
Jakub Lansky
0ffd97a183
Fixed typo in Spam folder name 2024-12-04 17:14:00 +01:00
Jakub Lansky
2143051eaf
Finished czech translation of Snappymail 2024-12-04 14:42:06 +01:00
Maarten
7eaa065d76
Merge pull request #1843 from edlerd/add-spf-dmarc
Add dmarc and spf indicators in mail view
2024-11-26 03:01:32 +01:00
the-djmaze
7a465606bc Resolve #1849 2024-11-26 02:47:17 +01:00
the-djmaze
cd3398db20 Resolve #1850 2024-11-26 01:47:34 +01:00
David Edler
18da535643
Show icon if no information is avilable on dkip, dmark or spf
Signed-off-by: David Edler <david.edler@canonical.com>
2024-11-18 17:20:57 +01:00
Maarten
d301b93b0d
Merge pull request #1845 from SergeyMosin/compact-composer-plugin-v1.0.6
Update squire to v2.3.2 and fix willPaste listener/sanitizer
2024-11-18 16:01:52 +01:00
the-djmaze
66553a36da Improvements for #1846 2024-11-18 16:00:03 +01:00
Sergey Mosin
95af4f60ea update squire to v2.3.2 and fix willPaste listener/sanitizer 2024-11-16 18:16:32 -05:00
David Edler
3ee978484a
Add dmarc and spf indicators in mail view #1820
Signed-off-by: David Edler <david.edler@canonical.com>
2024-11-12 18:03:38 +01:00
the-djmaze
8b5547104d Only allow Proxy for logged in users 2024-11-12 00:35:51 +01:00
the-djmaze
0d5d403fb8 Resolve #1825 2024-11-12 00:00:11 +01:00
the-djmaze
91ff43e6ab Resolve #1828 2024-11-11 23:57:41 +01:00
the-djmaze
bc6a2b67c0 Cleanup KnockoutJS a bit 2024-10-30 01:08:18 +01:00
the-djmaze
4cd5774257 ko.utils.domNodeDisposal.addDisposeCallback to ko.addDisposeCallback 2024-10-29 22:19:26 +01:00
the-djmaze
c744b83480 Put knockout bind context Proxy outside bindingFunction 2024-10-29 17:16:22 +01:00
the-djmaze
3d55bdaf5a Added test contacts CardDAV settings 2024-10-29 13:17:30 +01:00
the-djmaze
2809e75307 Preload identicon on delayed avatar lookup 2024-10-29 11:33:46 +01:00
the-djmaze
a83c3aacf8 Set identicon in cache when loading remote avatar fails 2024-10-29 01:29:52 +01:00
the-djmaze
8645b0ab1e Speedup identicons by caching them 2024-10-29 01:20:34 +01:00
the-djmaze
490d808cdb Speedup handling of identical avatars 2024-10-29 00:59:28 +01:00
the-djmaze
f12de9a3fd Improve contacts layout 2024-10-28 12:02:50 +01:00
the-djmaze
1b4bf3b8c3 Rename Identity.formattedName to Identity.toString 2024-10-28 12:01:59 +01:00
the-djmaze
64e780e500 Resolve #1813 2024-10-27 13:26:05 +01:00
the-djmaze
709d67226c Resolve #1815 2024-10-27 13:08:52 +01:00
Maarten
cab3b2b78a
Merge pull request #1822 from ner00/master
Update Portuguese
2024-10-27 12:39:19 +01:00
ner0
c50edb826b Update Portuguese 2024-10-23 12:09:55 +01:00
Maarten
332f9a79e3
Merge pull request #1816 from romawkafor/master
Please update user.json in Ukraine language.
2024-10-23 01:28:27 +02:00
the-djmaze
534b71e5a4 On reply, set from field to the received email to field #1805 2024-10-23 01:27:29 +02:00
the-djmaze
f0b3d798bc IdentityModel extends EmailModel 2024-10-23 00:56:42 +02:00
the-djmaze
b01072e2e3 Improved some templates 2024-10-23 00:52:36 +02:00
the-djmaze
bce9758338 Fix Knockout binding non-Observable notice 2024-10-23 00:52:01 +02:00
the-djmaze
3a589e240c Proper fix for default sortMode 2024-10-23 00:39:01 +02:00
the-djmaze
6bdd509b48 Cleanup knockout a bit 2024-10-23 00:37:48 +02:00
the-djmaze
4f6783a991 Bugfix: sortMode undefined at login 2024-10-23 00:28:13 +02:00
the-djmaze
012c1102f9 Merge branch 'master' of github.com:the-djmaze/snappymail 2024-10-15 22:00:53 +02:00
the-djmaze
78547e8c68 Resolve #1806 2024-10-15 22:00:33 +02:00
Roman Pereviznyk
fdc1a1c924
Update user.json
Minnor changes
2024-10-15 16:55:09 +02:00
Maarten
f877b849eb
Merge pull request #1810 from romawkafor/master
Corrections and addition of Ukrainian language localization files
2024-10-15 16:05:09 +02:00
Roman Pereviznyk
a1f890e3b4
Update user.json
I am adding some changes
2024-10-15 00:23:40 +03:00
Roman Pereviznyk
7aa87da449
Create admin.json
I am adding the possibility to use the Ukrainian language for the admin panel
2024-10-14 19:03:36 +03:00
Roman Pereviznyk
64f74d0511
Update zh.ini
Removed the typo in lines 15 and 16 (the \n symbol at the end of the tape)
2024-10-14 17:53:38 +03:00
Roman Pereviznyk
5d679b5be5
Update it.ini
Removed the typo in lines 15 and 16 (the \n symbol at the end of the tape)
2024-10-14 17:52:27 +03:00
Roman Pereviznyk
6bc658b5e0
Update hu.ini
Removed the typo in lines 15 and 16 (the \n symbol at the end of the tape)
2024-10-14 17:51:57 +03:00
Roman Pereviznyk
bc6de6c336
Update en.ini
Removed the typo in lines 15 and 16 (the \n symbol at the end of the tape)
2024-10-14 17:51:08 +03:00
Roman Pereviznyk
583528c448
Update cs.ini
Removed the typo in lines 15 and 16 (the \n symbol at the end of the tape)
2024-10-14 17:50:38 +03:00
Roman Pereviznyk
6307470ac0
Create uk.ini
Created a localization file for the Ukrainian language for the two-factor authentication application
2024-10-14 17:48:18 +03:00
Roman Pereviznyk
35914c0406
Create uk.ini
Created a localization file for the Ukrainian language for the password change application.
2024-10-14 17:32:59 +03:00
Roman Pereviznyk
5861124c2a
Update user.json
I propose changes for the Ukrainian language, taking into account syntax and untranslated phrases.

In a short time, I will try to create and translate an admin page (admin.json) and 2-Step Verification (TOTP), which I will also offer
2024-10-14 13:50:31 +03:00
Maarten
0ef2987911
Merge pull request #1799 from hguilbert/patch-31
Update user.json
2024-10-13 18:19:44 +02:00
Maarten
3d970e4706
Merge pull request #1800 from tinola/master
Update user.json
2024-10-13 18:19:16 +02:00
tinola
838e73fa94
Update user.json
Update Polish translation
2024-10-11 07:43:08 +02:00
hguilbert
42c98d42e0
Update user.json 2024-10-11 05:54:05 +02:00
the-djmaze
055c0d3bf4 Added setting for default messages sort mode 2024-10-11 00:28:05 +02:00
the-djmaze
16106a28d5 Resolve #1798 2024-10-10 23:45:28 +02:00
the-djmaze
9eb7f38485 KnockoutJS secure bindings using Proxy 2024-10-10 00:54:30 +02:00
the-djmaze
82ab4c4c6d Use CSS instead of $index #1797 2024-10-10 00:54:19 +02:00
the-djmaze
e345550c7f v2.38.2 2024-10-09 21:41:01 +02:00
the-djmaze
904a353bff Secure ko template bindings? 2024-10-09 21:35:34 +02:00
the-djmaze
cb9e1b0705 Resolve #1797 2024-10-09 21:34:00 +02:00
the-djmaze
ececfae31a v2.38.1 2024-10-08 23:07:58 +02:00
Maarten
156481583d
Merge pull request #1796 from SergeyMosin/compact-composer-plugin-v1.0.5
Compact composer plugin v1.0.5
2024-10-08 22:54:41 +02:00
the-djmaze
b86743de24 Resolve #1706 2024-10-08 22:52:33 +02:00
Sergey Mosin
e7ecf53b4a bump version to v1.0.5 2024-10-08 15:10:01 -04:00
Sergey Mosin
48b0519cb4 use 'Utils.cleanHtml' as pasteSanitizer + minor tweaks 2024-10-08 15:09:56 -04:00
Sergey Mosin
0a4cea5e4c make sure that KO bindings are re-applied 2024-10-08 15:09:51 -04:00
the-djmaze
6f2f40b983 Prepare for #1793 2024-10-08 17:49:21 +02:00
the-djmaze
28a8e3c157 Added the proper logging for #1706 2024-10-08 13:06:43 +02:00
the-djmaze
ba85eda7f0 Remove #1737 2024-10-07 00:03:35 +02:00
the-djmaze
ef724b4b4a Resolve #1775 2024-10-06 23:46:53 +02:00
the-djmaze
03d99f19c9 Resolve #1765 2024-10-06 23:46:36 +02:00
the-djmaze
f41ee21dba Merge branch 'master' of github.com:the-djmaze/snappymail 2024-10-06 17:33:04 +02:00
the-djmaze
d6ef5ff70a Squire paste image working again.
https://github.com/the-djmaze/snappymail/issues/1389#issuecomment-2389678680
2024-10-06 17:32:47 +02:00
Maarten
0520a5421a
Merge pull request #1790 from gnilebein/45819--Adjust-Boarder-Radius
Reduce border radius element to 8 px
2024-10-04 00:06:48 +02:00
Patrick Niebeling
907a9cefec Remove ""
Signed-off-by: Patrick Niebeling <patrick.niebeling@adacor.com>
2024-10-03 11:34:55 +02:00
Maarten
fafaaf21b1
Merge pull request #1788 from tinola/master
update Polish translations
2024-10-02 22:41:21 +02:00
Patrick Niebeling
115f8c8374 In Order to https://github.com/nextcloud/server/pull/45819 this change will adjust boarders to the new smaller boarder radius. 2024-10-02 14:32:03 +02:00
tinola
3f8f17a62e
Update admin.json
update Polish translation (admin page)
2024-10-02 07:33:07 +02:00
tinola
d3de9fae6b
Update user.json
update Polish translation
2024-10-02 07:29:08 +02:00
the-djmaze
0ecd0523f3 Merge branch 'master' of github.com:the-djmaze/snappymail 2024-10-02 01:52:30 +02:00
the-djmaze
68045f50f6 Resolve #1787 2024-10-02 01:52:15 +02:00
Maarten
b3d6c33257
Merge pull request #1785 from mstolf/bugfix_translate_ptBr
Fixed the translation of the fields MIDDLE_NAME, NAME_PREFIX, NAME_SU…
2024-10-01 18:06:59 +02:00
the-djmaze
f7f8d2d542 Bugfix the new msgId handling due to Squire sanitizeToDOMFragment using cleanHTML 2024-10-01 18:05:26 +02:00
the-djmaze
584728bc89 Squire: improved handling of BR elements 2024-10-01 17:56:43 +02:00
Matheus Stolf
4b802c0cb3 Fixed the translation of the fields MIDDLE_NAME, NAME_PREFIX, NAME_SUFFIX to pt-BR 2024-09-30 16:06:49 -03:00
the-djmaze
f43c2c92b5 Squire sanitizeToDOMFragment use cleanHTML 2024-09-30 17:01:50 +02:00
the-djmaze
beb418080f Squire: prevent collapse of empty elements so that the caret can be placed in there.
This eliminates the use of hacky <br> inside.
2024-09-30 16:55:01 +02:00
the-djmaze
9b5eaf4b5f Bugfix: Composer dialog "from" triangle wrong position due to font changes 2024-09-30 14:58:53 +02:00
the-djmaze
126208166d Bugfix: composer dialog scroll got broken in v2.28 2024-09-30 14:57:46 +02:00
the-djmaze
dc7eed4c80 Resolve #1780 2024-09-29 12:01:28 +02:00
the-djmaze
198cc852c3 O365 option to use Azure or Personal accounts #1645 2024-09-29 11:50:00 +02:00
the-djmaze
3931c11255 Added extension info to Admin -> Extensions -> Popup 2024-09-24 16:53:46 +02:00
the-djmaze
85ac404722 Drop support for noembed and noframes
Also remove everything after `<` inside `<style>`
2024-09-24 16:09:38 +02:00
the-djmaze
6b03820338 Resolve #1746 2024-09-24 15:54:55 +02:00
the-djmaze
c14bbab945 KnockoutJS remove with($context) scope for faster access 2024-09-24 13:55:07 +02:00
djmaze
932525566f Keep 1 space between HTML elements 2024-09-24 11:45:36 +02:00
djmaze
1b0dd48cdb KnockoutJS don't throw error when property is not observable 2024-09-24 11:45:17 +02:00
djmaze
3893849d21 Fix Sieve math-type and comparator handling 2024-09-24 11:44:40 +02:00
the-djmaze
92a6027aa0 Improved Knockout property binding error 2024-09-23 17:02:51 +02:00
the-djmaze
4022d39f76 Some Sieve parser improvements 2024-09-23 16:50:52 +02:00
the-djmaze
61c2eefec1 Fix Sieve QuotedString handling 2024-09-23 13:43:24 +02:00
the-djmaze
257d9201f6 Improved Sieve parser validation 2024-09-23 11:23:48 +02:00
the-djmaze
c773b852a1 Improve sieve parser 2024-09-23 10:48:22 +02:00
Maarten
0a3a5f6d59
Merge pull request #1769 from hguilbert/patch-30
Update user.json
2024-09-23 03:57:24 +02:00
Maarten
ce3596f299
Merge pull request #1771 from tinola/master
Update user.json
2024-09-23 03:56:44 +02:00
tinola
279cad0385
Update user.json 2024-09-22 22:30:29 +02:00
the-djmaze
d80fe8bc73 fix spacing 2024-09-22 19:02:57 +02:00
the-djmaze
109a110202 Resolve #1770 2024-09-22 19:02:17 +02:00
the-djmaze
93cc623891 Bugfix for #1709 2024-09-22 18:55:19 +02:00
the-djmaze
04f3133f27 Some small idea changes 2024-09-22 11:55:57 +02:00
hguilbert
f7b1981bcc
Update user.json 2024-09-22 07:12:04 +02:00
the-djmaze
3576c22401 Resolve #1726 2024-09-22 01:44:41 +02:00
the-djmaze
88a66d64f1 Resolve #1761 2024-09-22 01:28:23 +02:00
Maarten
7ba8de13e4
Merge pull request #1768 from ner00/master
Update Portuguese
2024-09-22 00:50:53 +02:00
ner0
5c45cf3465 Update Portuguese 2024-09-21 20:56:27 +01:00
the-djmaze
d3ecfdc71e Changes for #1706 2024-09-21 20:15:14 +02:00
the-djmaze
56db74986f Resolve #1716 2024-09-21 00:28:13 +02:00
the-djmaze
a4299c5f1c Fix extensions with login.credentials hook parameters 2024-09-21 00:01:02 +02:00
the-djmaze
2341dd9f62 Added mapping for sSmtpUser 2024-09-20 23:48:49 +02:00
the-djmaze
5056ecf493 Bugfix: ClientException #1706 2024-09-20 23:33:25 +02:00
the-djmaze
16cc17af00 Change for #1706 to debug more 2024-09-19 00:15:04 +02:00
Maarten
e3c53f8044
Merge pull request #1759 from bilogic/case-correction
Case correction
2024-09-18 23:11:38 +02:00
bilogic
5f1dd7f150
fix lv 2024-09-18 13:14:45 +08:00
bilogic
2f0732df81
fix en 2024-09-18 13:14:22 +08:00
the-djmaze
74c7948e72 Improved ical parsing for "view-ics" extension 2024-09-18 00:13:39 +02:00
the-djmaze
33b509c0a6 Added Admin - Extensions search filter 2024-09-17 21:16:17 +02:00
the-djmaze
365392eefe Bugfix: Admin - Config search should be ko.observable() not ko.observableArray() 2024-09-17 21:12:28 +02:00
the-djmaze
32da821c17 Replaced ical.es5.min.cjs with ical.js v2.1.0 for #1745 2024-09-17 20:57:45 +02:00
the-djmaze
b5ff818ca4 Bugfix: event.organizer could be undefined #1745 2024-09-17 20:53:55 +02:00
Maarten
02ef7c8d90
Merge pull request #1745 from Murena-SAS/dev/new-view-ics-plugin
Display ICS preview using an ICS library
2024-09-17 18:15:21 +02:00
Phie
e354fcb3bf new plugin for ics view, based on ical lib 2024-09-17 17:29:40 +02:00
the-djmaze
4e3a7a6de2 PHP 8.4: Implicitly nullable parameter declarations deprecated 2024-09-17 15:58:37 +02:00
the-djmaze
0263f8fe09 Use the custom Squire as submodule vendors/squire2 2024-09-17 15:00:28 +02:00
the-djmaze
a6ff022c0e Resolve #1754 2024-09-17 10:09:57 +02:00
the-djmaze
bd2ffe5078 v2.38.0 2024-09-16 20:51:26 +02:00
Maarten
96b7f9d4af
Update SECURITY.md 2024-09-16 17:07:53 +02:00
the-djmaze
cfbc47488a cleanHtml use allowedTags instead of disallowedTags and improved CSS handling 2024-09-16 14:37:26 +02:00
the-djmaze
05812c6be1 Updated custom Squire 2.3.2 2024-09-16 14:30:54 +02:00
the-djmaze
18452cc53c Also prevent error for #1733 2024-09-16 14:28:19 +02:00
the-djmaze
e0236ea52d Resolve #1733 2024-09-16 14:25:09 +02:00
the-djmaze
6f4f6bfd03 Improved attempt for #1746 2024-09-16 13:49:59 +02:00
the-djmaze
f8520c27a2 Bump version number 2024-09-16 10:17:26 +02:00
the-djmaze
61e7c00b62 Resolve #1746 2024-09-16 10:15:30 +02:00
the-djmaze
ed41d8e45f Implemen #1737 including an i18n fix 2024-09-15 18:56:30 +02:00
the-djmaze
80f3331187 Update Squire with SnappyMail changes 2024-09-15 18:31:30 +02:00
the-djmaze
9a7ff9fe27 Resolve #1715 2024-09-15 17:42:17 +02:00
the-djmaze
12b6ed3dbb Improved SquireUI handling of pathChange and bugfix bold match 2024-09-15 17:40:00 +02:00
the-djmaze
48acb9ca38 Squire bugfixes 2024-09-15 14:41:37 +02:00
the-djmaze
deae36d8c8 Use Squire 2.3.2 from https://github.com/the-djmaze/Squire/commits/snappymail/ 2024-09-15 12:59:58 +02:00
the-djmaze
947ce59eb9 RTL language improvement from #1744 by @rezaei92 2024-09-15 12:56:07 +02:00
Maarten
9be861dbc0
Merge pull request #1749 from hguilbert/patch-29
Update user.json
2024-09-14 17:10:25 +02:00
hguilbert
c58b89d74a
Update user.json 2024-09-14 14:59:30 +02:00
the-djmaze
8fb7067531 Added option to convert HTML to Markdown instead of plain, using Turndown #1604 2024-09-14 14:00:36 +02:00
the-djmaze
d3b0d6ca57 Debug Turndown ES2020 #1604 2024-09-14 13:37:31 +02:00
the-djmaze
0db6c6ab5f Bugfix: use_local_proxy_for_external_images is part of labs, not security
@SonarSource
2024-08-30 01:50:03 +02:00
the-djmaze
d0fb4ee2f6 Improved clear cookie handling 2024-08-30 01:47:28 +02:00
the-djmaze
366ca05200 Bugfix: application/pgp-encrypted attachments not listed 2024-08-30 00:39:00 +02:00
the-djmaze
ed7a2bd239 Resolve #1725 2024-08-28 00:34:38 +02:00
the-djmaze
6ebf273390 Resolve #1724 2024-08-28 00:24:28 +02:00
Maarten
9d488b0e61
Merge pull request #1722 from ner00/master
Update Portuguese
2024-08-27 23:42:48 +02:00
the-djmaze
082dd3af41 Bugfix: Call to a member function Email() on null #1706 2024-08-27 23:39:27 +02:00
ner0
589606eef0 SETTINGS_GENERAL/MAILTO 2024-08-27 17:35:44 +01:00
ner0
1c1682a192 Update Portuguese 2024-08-27 17:23:45 +01:00
the-djmaze
1ad946d013 v2.37.3 2024-08-27 16:47:44 +02:00
the-djmaze
5e4520e10c Force Nextcloud personal language by default #1428 2024-08-27 16:20:39 +02:00
the-djmaze
1ac4f05d91 Sabre/Xml v4.0.5 2024-08-27 15:27:44 +02:00
the-djmaze
9e3d6c6e23 Show warning in Admin -> About when PHP runs in 32bit 2024-08-27 13:38:42 +02:00
the-djmaze
3306fadc47 MS autodiscover priorities DNS over subdomain 2024-08-27 13:19:30 +02:00
the-djmaze
9e2b9a9fcd Show GnuPG version for #1560 2024-08-27 12:12:33 +02:00
the-djmaze
780e1f9b92 Make sure only scalar values are allowed in $_ENV #1719 2024-08-27 11:49:11 +02:00
the-djmaze
547953eb31 Resolve #1718 2024-08-26 20:45:32 +02:00
the-djmaze
59652d2d61 Mark images with width=1 as tracking pixel 2024-08-26 13:22:41 +02:00
the-djmaze
daf02e53f3 Resolve #1706 2024-08-26 11:20:07 +02:00
Maarten
4a8fea8456
Merge pull request #1717 from hguilbert/patch-28
Update user.json
2024-08-26 11:06:12 +02:00
hguilbert
a442322729
Update user.json 2024-08-26 06:14:29 +02:00
the-djmaze
34f730a196 Added markedjs for #1604 2024-08-25 18:04:59 +02:00
the-djmaze
cb7dafba4d Cleanup Cacher handling and added some documentation 2024-08-25 17:35:14 +02:00
the-djmaze
9c0d31997a Fix CRLF #1712 2024-08-25 15:56:11 +02:00
the-djmaze
2ac71fe4ad Localization strings for ACL #157 2024-08-25 15:54:37 +02:00
the-djmaze
5b2b7b0294 Parse error #1712 2024-08-25 15:52:41 +02:00
the-djmaze
612b5a81d2 Simplify sieve scripts list #1675 2024-08-22 00:41:14 +02:00
the-djmaze
0dda102a05 Added turndown.js for #1604 2024-08-21 01:46:31 +02:00
the-djmaze
113003f485 Improved domain autoconfig interaction 2024-08-21 00:28:36 +02:00
the-djmaze
6a1b83b365 Resolve #1678 2024-08-20 16:30:25 +02:00
the-djmaze
7ff5232554 Resolve #1574 2024-08-20 13:58:10 +02:00
the-djmaze
a761bdcaaa Small cleanup of #157 2024-08-20 13:12:14 +02:00
the-djmaze
c09106472f Small UI fix for #157 2024-08-20 13:10:50 +02:00
the-djmaze
e5a176f5ea Added editing of ACL rules #157 2024-08-20 13:05:06 +02:00
Maarten
5987ccdbc9
Merge pull request #1712 from Artoria2e5/zh-update
Update zh, zh-TW translations
2024-08-19 15:09:50 +02:00
the-djmaze
423f5ba41d Resolve #1709 2024-08-16 23:01:50 +02:00
the-djmaze
d00c953c88 Change handling of (token) errors due to #1706 2024-08-16 22:05:15 +02:00
the-djmaze
02ea6c4359 Resolve #1710 2024-08-16 21:19:28 +02:00
Mingye Wang
2aeeb701c8 Update zh, zh-TW translations
This commit updates zh and zh-TW translations. I try to match the language used in the file, but I still hope a native speaker from the other side of the strait can check my work.
2024-08-15 18:33:40 +08:00
the-djmaze
1395d506cf v2.37.2 2024-08-13 11:30:00 +02:00
the-djmaze
e22414d908 Resolve #1645 2024-08-13 01:14:15 +02:00
the-djmaze
17c4f87fa5 v2.37.2 for Nextcloud 2024-08-13 00:46:25 +02:00
the-djmaze
3ba75fecba Resolve #1703 2024-08-13 00:39:06 +02:00
the-djmaze
d87c46ff03 Show content-type error on login 2024-08-13 00:17:04 +02:00
the-djmaze
6ad92b9efd Prevent logout loop on error 2024-08-13 00:03:12 +02:00
the-djmaze
6d477aa6ec Validate Fetch JSON response for #1703 2024-08-13 00:02:51 +02:00
the-djmaze
0dedeb0180 v2.37.1 2024-08-12 20:46:00 +02:00
the-djmaze
cc9bc3d6da Bugfix: Gulp v5 broke copying fonts.
https://github.com/gulpjs/gulp/issues/2797
2024-08-12 20:42:03 +02:00
the-djmaze
03a8e6a3ec v2.37.0 2024-08-12 18:02:05 +02:00
the-djmaze
486bbba4ca Bugfix: Attempt to read property "smimeSigned" on null 2024-08-12 17:08:36 +02:00
the-djmaze
55d7d52e35 Use PHP_SAPI 2024-08-12 17:07:44 +02:00
the-djmaze
8c6f0adc66 Improved handling of RFC 2822 dates #1694 2024-08-12 16:50:11 +02:00
the-djmaze
13cb08f64a FileStorage strict properties 2024-08-12 15:24:52 +02:00
the-djmaze
83a71b089c Don't use focusedMessage for active menu items, it could confuse 2024-08-12 15:24:30 +02:00
the-djmaze
6c9812ee7f Drop md5(file_get_contents(DATA.php)) because i think nobody used this undocumented feature 2024-08-11 12:39:34 +02:00
Maarten
22988b4bbc
Merge pull request #1699 from Murena-SAS/dev/nc-oauth-multiaccount
allow multi-account in nc with oauth login
2024-08-11 12:03:44 +02:00
Maarten
ec8da2b72b
Merge pull request #1698 from Murena-SAS/dev/fix-set-capabilities
Fix capabilities when THREAD is disabled
2024-08-11 12:01:34 +02:00
the-djmaze
a969179cbf Make #1689 optional through application.ini 2024-08-11 12:00:24 +02:00
Akhil
50265b3b37 Also perform logout if explicit creds change 2024-08-08 22:09:34 +05:30
Akhil
ae74f21242 Also check if the account has been defined in setting 2024-08-08 21:58:56 +05:30
Akhil
25428c7e9d Merge branch 'master' of https://github.com/Murena-SAS/snappymail into dev/nc-oauth-multiaccount 2024-08-08 21:21:41 +05:30
the-djmaze
97c0f81a2e allow multi-account in nc with oauth login 2024-08-08 21:17:56 +05:30
Akhil
b8f7f751b0 Fix capabilities when THREAD is disabled
Signed-off-by: Akhil <akhil@e.email>
2024-08-08 20:40:14 +05:30
the-djmaze
c33dd8f856 Centralize tryToClose() for all popups 2024-08-06 21:41:52 +02:00
the-djmaze
fa15742edb Ask for closing Identity popup when empty and not saving #1689 2024-08-06 21:28:20 +02:00
the-djmaze
aa52d2e8f8 Resolve #1689 2024-08-06 17:50:43 +02:00
the-djmaze
3fb700f163 Add exception trace to log for #1648 2024-08-06 16:42:38 +02:00
the-djmaze
2f90d95c4c Set author for #1673 2024-08-06 13:40:43 +02:00
the-djmaze
b3a94c3524 Nextcloud extension bump version number 2024-08-06 13:27:27 +02:00
the-djmaze
15f4ba54df Bugfix: Nextcloud language detection 2024-08-06 13:26:36 +02:00
the-djmaze
505e6e2a0d Improve RFC 2822 date parser due to PHP createFromFormat() shortcomings
https://github.com/the-djmaze/snappymail/issues/1694#issuecomment-2270983942
2024-08-06 12:50:24 +02:00
the-djmaze
c0ed491f7a Bugfix: Division by zero #1694 2024-08-06 12:25:39 +02:00
the-djmaze
166b790a3e Preparations for #1689 2024-08-06 12:19:17 +02:00
the-djmaze
d2f3aa1c10 Resolve #1690 2024-08-06 12:02:23 +02:00
the-djmaze
4e8f12a392 Resolve #1691 2024-08-06 11:49:46 +02:00
the-djmaze
cc04a82bfe Resolve #1686 2024-08-06 11:40:25 +02:00
the-djmaze
3a8af7de29 Some bugfixes for #1673 2024-08-05 17:20:41 +02:00
Maarten
85fb2ff44d
Merge pull request #1673 from AbdoBnHesham/master
Add Search Filters Plugin with Gmail-like Functionality
2024-08-05 16:57:36 +02:00
Maarten
f89a8c3eb5
Merge pull request #1688 from cuttlefish-uk/master
Add change-password-mailcow driver
2024-08-05 16:54:18 +02:00
the-djmaze
de26a00c49 Improvement #1657 2024-08-05 16:46:53 +02:00
Jude Venn
f21dd94f2c Add change-password-mailcow driver 2024-07-25 10:37:09 +01:00
AbdoBnHesham
4a56d2d087 Add Search Filters Plugin with Gmail-like Functionality
Introducing a new plugin for enhanced email organization: Search
Filters, inspired by similar features in Gmail (Search Filters) and
Outlook (Rules). The plugin allows you to define custom search queries
that automatically perform one or more actions on matching emails based
on their priority level (1-10). Available actions include marking as
seen, flagging/starring, and moving to a designated folder.

This plugin is designed to work seamlessly with both old and new emails
in your inbox, taking into account the specified search query criteria.
However, please be aware of an outstanding bug that affects the display
of unread email counts beside folders; this issue has yet to be
resolved.

I have developed this plugin as a freelance project for a client on
their custom server setup. Unfortunately, I never received any feedback
or response from my client after completion of the work. Nonetheless, I
hope that sharing it here will benefit others and contribute to the
ongoing advancement of this open-source project.

Please note: This plugin is offered as-is with no warranties or
guarantees. Make sure to thoroughly test and verify its functionality
before using it in a production environment. Thank you to everyone who
has contributed to making this project a reality.
2024-07-18 23:49:37 +03:00
the-djmaze
e4316e6665 Resolve #1666 2024-07-15 19:27:48 +02:00
the-djmaze
ad81e3f9e9 Added mime types multipart/report and message/global-delivery-status 2024-07-15 19:27:26 +02:00
the-djmaze
d10e4ba22a Merge branch 'master' of github.com:the-djmaze/snappymail 2024-07-15 16:13:51 +02:00
the-djmaze
becaaaa2fd Resolve #1654 2024-07-15 16:13:32 +02:00
Maarten
082f1058ce
Merge pull request #1667 from hguilbert/patch-27
Update user.json
2024-07-15 14:44:10 +02:00
Maarten
0b495b1fa5
Merge pull request #1668 from tinola/master
Update user.json
2024-07-15 14:43:47 +02:00
the-djmaze
3161961faa Resolve #1663 2024-07-15 14:42:02 +02:00
tinola
9303c48e01
Update user.json 2024-07-15 08:35:08 +02:00
hguilbert
b214ba0054
Update user.json 2024-07-15 05:49:16 +02:00
the-djmaze
2ea68e88b0 Translate "UNDELETE" in Dutch 2024-07-14 23:29:32 +02:00
the-djmaze
93869315c1 Drop message.deleted observable 2024-07-14 23:29:12 +02:00
the-djmaze
d7bf80ea79 Merge branch 'master' into mark-for-deletion-issue-1657 2024-07-14 23:09:48 +02:00
the-djmaze
3ff2c4fb82 Hide undeleteCommand command when deleted messages are hidden #1657 2024-07-14 23:04:15 +02:00
the-djmaze
b70860c903 Only set flag \Deleted when Trash is set to "do not use" 2024-07-14 22:55:04 +02:00
the-djmaze
dc49a00f5f Bugfix: Call to a member function SmtpSettings() on null #1664 2024-07-14 14:21:55 +02:00
the-djmaze
50e30dbb6e Slightly modified patch code of #1657 2024-07-11 01:31:58 +02:00
the-djmaze
95436347e5 Resolve #1660 2024-07-11 00:25:26 +02:00
the-djmaze
57d1021d51 Improved cache handling of remote avatars (delayed or not) 2024-07-08 18:26:08 +02:00
the-djmaze
10450dea8b Cleanup localizations 2024-07-08 12:49:38 +02:00
the-djmaze
37fd42448f Translate dutch 2024-07-08 12:07:51 +02:00
Maarten
04607318ac
Merge pull request #1655 from hguilbert/patch-26
Update user.json
2024-07-08 10:54:40 +02:00
the-djmaze
ed0223fbb8 Improved InvalidToken debugging 2024-07-08 10:53:03 +02:00
hguilbert
823ad4777f
Update user.json 2024-07-08 05:47:56 +02:00
the-djmaze
34a8ff3ce3 Resolve #1649 2024-07-08 01:23:04 +02:00
the-djmaze
637ec00d8f Removed abandoned vulnerable rollup-plugin-html 2024-07-08 01:08:40 +02:00
the-djmaze
1a19d25fdb Updated gulp to v5 and replaced vulnerable gulp-header with gulp-append-prepend 2024-07-08 00:43:48 +02:00
the-djmaze
cd5ba2b000 transitionend listen to opacity as top changed on resize of view 2024-07-07 23:33:29 +02:00
the-djmaze
f644430a78 console.log the mailto handler 2024-07-07 18:34:23 +02:00
the-djmaze
a3d9d98184 Made registerProtocolHandler('mailto') optional by activating at Settings -> General 2024-07-07 18:16:39 +02:00
the-djmaze
325197175b Bugfix: prevent multiple afterShow() and afterHide() due to transitionend on multiple properties 2024-07-07 16:31:34 +02:00
the-djmaze
60c35f7c12 Added rl-vm-visible to the extensions README 2024-07-07 16:30:35 +02:00
the-djmaze
f8d358a4f3 Added JavaScript rl-vm-visible event so that demo extension can prefill passphrase 2024-07-07 16:28:29 +02:00
the-djmaze
7a1652972d Detailed error message on account switch failure for #1594 2024-07-07 15:54:48 +02:00
the-djmaze
288ff23f2b Improved InvalidToken handling for #1653 2024-07-07 15:48:26 +02:00
Maarten
7f64dee54b
Merge pull request #1651 from tkasch/master
German translation update
2024-07-07 01:27:28 +02:00
the-djmaze
9771eced99 Align save button in admin security settings 2024-07-06 19:27:53 +02:00
the-djmaze
1a4e2c5d35 Nextcloud failed loading due to missing app_path 2024-07-06 19:27:22 +02:00
tkasch
54ef583c41
Update user.json
further improving German translation
2024-07-04 16:34:13 +02:00
tkasch
9fa5b6bf9e
Update user.json
further improve German translation
2024-07-04 16:21:39 +02:00
tkasch
8ceb4e8fb3
Update admin.json
further improve German translation
2024-07-04 15:17:54 +02:00
tkasch
7803bfe429
Update admin.json
further improve German translation
2024-07-04 14:59:03 +02:00
tkasch
63c5dbb93e
Update relativetimeformat.js
improve German translation
2024-07-04 14:41:31 +02:00
tkasch
c0a198faaa
Update admin.json
improve German translation
2024-07-04 14:36:30 +02:00
tkasch
448d5c4cfc
Update admin.json
improve German translation
2024-07-04 14:31:45 +02:00
tkasch
bebee2f32c
Update user.json
further improve German translation
2024-07-04 12:57:05 +02:00
tkasch
a39f09773b
Update user.json
further improve German translation
2024-07-04 12:28:04 +02:00
tkasch
4dc9dfa586
Update user.json
further improve German translation
2024-07-04 11:55:22 +02:00
tkasch
0893a2ff20
Update user.json
further improve German translation
2024-07-04 11:51:05 +02:00
tkasch
cc8ce3f679
Update user.json
further improving German translation
2024-07-04 11:43:55 +02:00
tkasch
ae5c1db83d
Update user.json
improve German translations
2024-07-04 10:00:26 +02:00
the-djmaze
acaa5bae55 #1645 forgot the workaround ?LoginO365 to /LoginO365 2024-07-02 13:22:59 +02:00
the-djmaze
f49f606b0e https://github.com/the-djmaze/snappymail/issues/1645#issuecomment-2202364675 2024-07-02 13:18:52 +02:00
the-djmaze
fad4dea1c5 Resolve #1647 2024-07-01 21:24:21 +02:00
the-djmaze
f177bb2f31 #1645 requires v2.36.5+ 2024-07-01 15:42:18 +02:00
the-djmaze
47d18cafa3 Workaround for #1645 2024-07-01 15:41:33 +02:00
the-djmaze
dcfe7d3f2d Draftcode Microsoft restrictions #1645 2024-07-01 12:53:01 +02:00
the-djmaze
60abc83ff1 #1645 Mail.ReadWrite 2024-07-01 12:10:37 +02:00
the-djmaze
139d3e6651 Small improvements for #1645 2024-07-01 12:09:49 +02:00
the-djmaze
7f7ee97106 Draft code for #1645 2024-07-01 11:21:02 +02:00
Maarten
9f3f41f308
Merge pull request #1643 from ner00/master
Update Portuguese
2024-06-29 22:39:49 +02:00
the-djmaze
eda85cb580 Resolve #1603 2024-06-29 22:34:42 +02:00
ner0
16f71ab219 Update Portuguese 2024-06-25 15:48:43 +01:00
the-djmaze
28aefdddda v2.36.4 2024-06-25 12:55:26 +02:00
the-djmaze
26cc746619 Resolve type errors #1638 2024-06-25 12:51:55 +02:00
the-djmaze
c872e5afe4 Disable OBJECTID capability by default due to #1640 2024-06-25 03:29:46 +02:00
the-djmaze
5a63b02782 Cleanup avatars nextcloud handling 2024-06-24 16:23:18 +02:00
the-djmaze
0b494a13a5 Added option to load known favicon of domainname 2024-06-24 16:06:02 +02:00
the-djmaze
700543c573 Changes for #1640 2024-06-24 13:43:02 +02:00
the-djmaze
e867000ff7 Idea for #1641 2024-06-24 12:26:48 +02:00
the-djmaze
b2b3dac35b Resolve #1640 workaround PHP issue 2024-06-23 12:49:05 +02:00
the-djmaze
8d11ad43da undefined dateTimestampSource 2024-06-23 12:48:33 +02:00
the-djmaze
614715f398 Resolve #1641 2024-06-23 12:31:08 +02:00
the-djmaze
021b332838 Log mailto protocol for #1635 2024-06-17 14:58:35 +02:00
the-djmaze
43f54b30c6 Resized Magnetic theme preview.png #1637 2024-06-17 14:49:24 +02:00
Maarten
e1b028652e
Merge pull request #1638 from huloza/patch-1
Update user.json Español
2024-06-17 14:44:18 +02:00
Maarten
944d1173d5
Merge pull request #1637 from TheCuteFoxxy/master
Add Theme: Magnetic
2024-06-17 14:41:28 +02:00
huloza
3e18c5286a
Update user.json Español
Translated several missing texts.
Traducidos varios textos faltantes.
2024-06-16 11:54:35 -04:00
Casey Barnett
d2ff0bcc0b Fix images
Fixed file cropping. Fixed image formats.
2024-06-15 17:34:29 -04:00
Casey Barnett
26c3a2c943 Add Magnetic theme 2024-06-15 17:31:10 -04:00
Maarten
5d52382c7a
Merge pull request #1632 from tinola/master
Update user.json
2024-06-11 13:22:30 +02:00
tinola
fdd1d98658
Update user.json 2024-06-11 07:21:29 +02:00
Maarten
009eb749fe
Merge pull request #1631 from hguilbert/patch-25
Update user.json
2024-06-11 01:21:52 +02:00
hguilbert
dfe5bba285
Update user.json 2024-06-10 17:29:22 +02:00
the-djmaze
70e4fedc4d Allow resize of compose window 2024-06-10 12:58:07 +02:00
the-djmaze
ca3d3b44ce Merge branch 'master' of github.com:the-djmaze/snappymail 2024-06-10 12:29:13 +02:00
the-djmaze
13c76d395e Added for #1545 2024-06-10 12:28:58 +02:00
Maarten
ca67e9fb61
Merge pull request #1629 from hguilbert/patch-24
Update user.json
2024-06-10 10:09:37 +02:00
Maarten
740e239d2e
Merge pull request #1630 from tinola/master
Update user.json
2024-06-10 10:09:16 +02:00
tinola
bc6abee577
Update user.json 2024-06-10 07:24:42 +02:00
hguilbert
5d657abd46
Update user.json 2024-06-10 06:00:20 +02:00
the-djmaze
a3fb5a30ee Translation strings for #1606 2024-06-09 17:14:13 +02:00
Maarten
13543e314e
Merge pull request #1606 from codiflow/patch-1
Add AdvancedSearch support for filtering mails before a given date
2024-06-09 17:09:19 +02:00
the-djmaze
4538123526 Solve *Decrypt does not exists of previous change 2024-06-09 17:07:01 +02:00
the-djmaze
3bac68dffb Improved language detection code 2024-06-09 17:06:35 +02:00
the-djmaze
02e6198f17 Improve Decrypt errors 2024-06-09 16:56:14 +02:00
the-djmaze
e0219c020a translate dutch 2024-06-09 16:55:53 +02:00
Maarten
92ed4fcc81
Merge branch 'master' into patch-1 2024-06-09 16:43:46 +02:00
Maarten
7beb7120a9
Merge pull request #1623 from tinola/master
Update admin.json
2024-06-05 21:04:53 +02:00
tinola
2daa299b72
Update admin.json 2024-06-05 07:27:47 +02:00
Maarten
3e7eb46f33
Merge pull request #1622 from hguilbert/patch-23
Update admin.json
2024-06-04 22:02:15 +02:00
hguilbert
b840e5244b
Update admin.json 2024-06-04 18:35:13 +02:00
the-djmaze
d40f211995 Added some info about the viewModelTemplateID in the example extension 2024-06-04 17:38:12 +02:00
the-djmaze
f4a11c9c6e Resolve #1607 2024-06-04 14:19:03 +02:00
the-djmaze
e68179c752 Add dateTimestampSource for #1554 2024-06-04 13:22:23 +02:00
the-djmaze
ba46e995cd Resolve #1608 2024-06-04 13:08:33 +02:00
the-djmaze
9954e25b65 Resolve #1609 2024-06-03 16:01:52 +02:00
the-djmaze
9633956b99 Fixed: Undefined $sEmail in DoAdminDomainMatch 2024-06-03 15:14:30 +02:00
the-djmaze
1acaa5d542 Prepare for #1606 2024-06-03 12:41:29 +02:00
Maarten
5323a1b00f
Merge pull request #1620 from Murena-SAS/access-token-updated
access token updated listener for OIDC LOGIN
2024-06-03 12:27:57 +02:00
Avinash Gusain
44c040707c access token updated listener 2024-05-31 11:40:31 +05:30
Maarten
ba400a9c4f
Merge pull request #1612 from cm-schl/master
Make plugin ldap-mail-accounts compatible with newer versions of SnappyMail
2024-05-28 17:51:50 +02:00
the-djmaze
6d15cabfe5 Improved signing messages by allowing to choose between the options 2024-05-28 17:50:56 +02:00
the-djmaze
c2e558c9b1 Resolve #1611 2024-05-28 17:45:47 +02:00
the-djmaze
c6b9f2276b Small change to report bug template 2024-05-28 16:43:49 +02:00
cm-schl
82fb6fe7e6 update readme and version number to 2.2.0 2024-05-28 16:31:09 +02:00
cm-schl
caf1ee3e2a
Merge branch 'the-djmaze:master' into master 2024-05-28 16:25:25 +02:00
the-djmaze
55b8620186 Merge branch 'master' of github.com:the-djmaze/snappymail 2024-05-28 11:53:30 +02:00
the-djmaze
998eb8c4bb Bugfix: default language error
https://github.com/the-djmaze/snappymail/issues/1579#issuecomment-2134782450
2024-05-28 11:53:15 +02:00
Maarten
614c7ace44
Merge pull request #1605 from ner00/master
Update Portuguese
2024-05-27 21:33:18 +02:00
Christian 🦄
92ab56bf5e
Add AdvancedSearch support for filtering older mails 2024-05-27 14:48:45 +02:00
the-djmaze
dd39d18e2a Changes #1603 2024-05-27 14:22:34 +02:00
ner00
99c79a8fa8 Update Portuguese 2024-05-27 12:52:13 +01:00
the-djmaze
047fd62ceb Hopefully solved Nextcloud login issues #1602 2024-05-27 13:43:16 +02:00
the-djmaze
68218e43dc v2.36.3 2024-05-27 11:19:37 +02:00
the-djmaze
365a5effdf UserAuth prevent plugin errors (like the Nextcloud plugin did) 2024-05-27 10:51:54 +02:00
the-djmaze
69cd876895 Bugfix: Nextcloud login failed due to undefined email address 2024-05-27 10:17:28 +02:00
the-djmaze
ab2f646496 Bugfix: Nextcloud getPrimaryEMailAddress() on null 2024-05-27 10:13:20 +02:00
the-djmaze
4bb1b41505 Bump version for #1597 2024-05-26 18:33:29 +02:00
Maarten
aa91623d35
Merge pull request #1597 from PhilippMundhenk/master
[ProxyAuth plugin] password as SensitiveString & support for subdirectories
2024-05-26 18:31:04 +02:00
the-djmaze
df1e5926df Bugfix: Undefined variable $aTokenData #1567 2024-05-26 18:25:20 +02:00
Philipp Mundhenk
6ef3390080 adding support for snappymail in subdirectories 2024-05-26 17:14:58 +02:00
Philipp Mundhenk
8d3f4f5e1d introduced SensitiveString handling 2024-05-26 17:04:32 +02:00
Philipp Mundhenk
92bfa7bd75
Merge pull request #1 from the-djmaze/master
updating from upstream
2024-05-26 17:00:22 +02:00
the-djmaze
429a0eb4e0 v2.36.2 2024-05-26 14:12:54 +02:00
the-djmaze
478087fe9e Updated Amazon avatar 2024-05-26 13:58:31 +02:00
the-djmaze
4dc7ad02d5 Updates for #1572 2024-05-26 13:49:45 +02:00
the-djmaze
b5dbd0cc7a OpenPGP.js automatically import backup keys from server 2024-05-26 12:53:35 +02:00
the-djmaze
4365892927 Resolve #1589 2024-05-21 18:08:04 +02:00
Maarten
4ad25188b2
Merge pull request #1572 from Murena-SAS/fix-oidc-sso-login
oidc login
2024-05-21 17:56:35 +02:00
Maarten
d069ca09a7
Merge pull request #1588 from Thadah/patch-1
Update Basque translation
2024-05-21 17:55:41 +02:00
the-djmaze
97391a3f33 Changes for #1587 2024-05-21 17:47:24 +02:00
cm-schl
263dbab22a reorder steps to speed up the process and make sure that removed additional accounts are deleted even if the user now has no additional account 2024-05-21 15:28:07 +02:00
Gontzal Pujana Onaindia
3d845f67ee
Update Basque translation 2024-05-20 17:04:26 +02:00
cm-schl
0cbeba29d1 update readme 2024-05-15 14:32:29 +02:00
cm-schl
e81cf80ac9 update readme 2024-05-15 14:19:31 +02:00
the-djmaze
c3c75c216b Resolve #1580 2024-05-15 00:22:38 +02:00
the-djmaze
0dcc468ae1 Resolve #1575 2024-05-15 00:05:43 +02:00
cm-schl
23fe3a946b Add new function to overwrite the cryptkey
This is needed to prevent SnappyMail to ask the user for the old (maybe forgotten) ldap password
2024-05-14 17:24:26 +02:00
cm-schl
f8af48cf47
Merge branch 'the-djmaze:master' into master 2024-05-14 15:48:41 +02:00
cm-schl
2786485de2 don't pass a smtp password to setCredentials
Password is not needed at the moment and passing it seams to create problems when sending mails
2024-05-13 17:25:46 +02:00
Maarten
ad3313fda4
Merge pull request #1578 from SergeyMosin/compact-composer-plugin-v1.0.4
Compact composer plugin v1.0.4
2024-05-13 12:51:49 +02:00
Maarten
c0c0106abe
Merge pull request #1573 from ner00/master
Update Portuguese
2024-05-13 12:51:00 +02:00
the-djmaze
34f5f83978 Bugfix SCRAM auth response #1575 2024-05-13 12:43:21 +02:00
cm-schl
1f5613b504
Merge branch 'the-djmaze:master' into master 2024-05-09 16:33:48 +02:00
Sergey Mosin
c0d3c352b8 version bump 2024-05-08 20:42:36 -04:00
Sergey Mosin
3c178a5d1d fix caret in the root node and wrong field focused on compose dialog open 2024-05-08 20:41:36 -04:00
Sergey Mosin
f33e2315fc update squire to v2.2.8 2024-05-08 20:34:43 -04:00
ner0
0dbf992494
Update Portuguese 2024-05-02 12:31:13 +01:00
Avinash Gusain
3d0b16600c oidc login 2024-05-02 13:52:32 +05:30
cm-schl
cda2aad98d small cleanup 2024-04-30 11:31:13 +02:00
cm-schl
1d8ed48666 remove debug messages 2024-04-30 08:58:29 +02:00
the-djmaze
6cafd65690 Bugfix Log RFC 2822 date parse error for #1554 2024-04-30 02:58:22 +02:00
the-djmaze
73ac962289 Log signal info for #1569 2024-04-30 02:26:20 +02:00
the-djmaze
cc360837b1 Improved "remember me" cookie handling 2024-04-29 21:21:07 +02:00
cm-schl
1ec4930dfa resolved problems arised from changes in v2.36 2024-04-29 17:48:47 +02:00
the-djmaze
e9f2b71574 Log RFC 2822 date parse error for #1554 2024-04-28 18:02:51 +02:00
the-djmaze
53a6f04f18 Added "copy to" action in menu's for #1559 2024-04-28 17:32:25 +02:00
the-djmaze
0aa48a8dd4 Prevent mkdir() error #1565 2024-04-28 14:50:08 +02:00
the-djmaze
685b2ea93a Fix GPG/PGP exec() return false handling 2024-04-28 14:49:48 +02:00
the-djmaze
f9b5a37555 Simple solution for #1562 2024-04-28 14:46:37 +02:00
Maarten
0c081cf9aa
Merge pull request #1562 from sevmonster/patch-1
Properly escape path separator in `tar.php` file list regex
2024-04-28 14:45:46 +02:00
the-djmaze
9ec8ac8bba Resolve #1561 2024-04-28 14:03:34 +02:00
the-djmaze
f4e81a183e Bugfix: Undefined variable $sFilename 2024-04-28 13:55:52 +02:00
sev
c73736a05f
properly escape path sep in tar file list regex 2024-04-26 10:51:28 -05:00
the-djmaze
4b63adb0d5 v2.36.1 2024-04-24 02:13:35 +02:00
the-djmaze
cd8b3936fb Move OAuth2 to core 2024-04-24 02:02:37 +02:00
the-djmaze
fdc019e43c Resolve #1557 2024-04-24 01:35:40 +02:00
the-djmaze
944edef596 password => passphrase 2024-04-23 18:41:41 +02:00
the-djmaze
7df1d55162 Nextcloud don't use RainLoop login credentials 2024-04-23 18:06:59 +02:00
the-djmaze
b7a11d42de Don't input password in command, ask in prompt #1552 2024-04-23 18:03:04 +02:00
the-djmaze
372e4bc1c0 Rename application.ini [labs]log_ajax_response_write_limit to [logs]json_response_write_limit 2024-04-23 17:44:13 +02:00
the-djmaze
17fc37975c Rename application.ini [labs]login_fault_delay to [login]fault_delay 2024-04-23 17:38:28 +02:00
the-djmaze
aaae0a314c Remove date_from_headers from application.ini and internalTimestamp 2024-04-23 17:35:16 +02:00
the-djmaze
2656d362d6 Added idea to ask SMTP credentials on AUTH failure 2024-04-23 15:10:45 +02:00
the-djmaze
24dbe4f197 AskPopupView.password default is to only ask passphrase 2024-04-23 14:54:48 +02:00
the-djmaze
d198ad5a59 Improved the compactcomposer #1498 2024-04-23 03:17:41 +02:00
the-djmaze
b4dbf249ee ChangePasswordPlugin also use Have I Been Pwned 2024-04-22 17:38:05 +02:00
the-djmaze
6739ec21c8 Bugfix haveibeenpwned suffix handling 2024-04-22 15:55:31 +02:00
the-djmaze
c9d3f3de4a Example haveibeenpwned integration 2024-04-22 15:38:14 +02:00
the-djmaze
027ce6ccae Resolve #1552 2024-04-22 13:56:53 +02:00
the-djmaze
1b0962cce1 GnuPG check if shell_exec is disabled 2024-04-22 13:00:22 +02:00
the-djmaze
325c748acc Check open basedir for #1555 2024-04-21 03:52:30 +02:00
the-djmaze
ff80235ee8 Put modified Squire more in line with v2.2.8 2024-04-17 01:45:51 +02:00
the-djmaze
e304b1dd26 Nextcloud merge oidcLogin into beforeLogin 2024-04-17 01:44:25 +02:00
the-djmaze
0ff66f8df8 set min-width for attachmentParent and flagParent to line them up 2024-04-17 01:43:53 +02:00
the-djmaze
f2c4b8a1b3 cPanel use extension login-cpanel instead of login-remote 2024-04-17 01:10:03 +02:00
the-djmaze
e554bcc27e Extend handling login credentials by allowing to modify SMTP username 2024-04-17 01:09:07 +02:00
the-djmaze
3a6a5a6925 small quotation change 2024-04-16 02:09:48 +02:00
the-djmaze
1592c84ac0 Handle RFC 5987 in Content-Disposition header 2024-04-16 02:09:24 +02:00
the-djmaze
f8790a3a89 Rename account IncLogin to ImapUser and OutLogin to SmtpUser 2024-04-15 00:24:11 +02:00
the-djmaze
326d35eee7 Bugfix: connect settings stripLogin domain failed 2024-04-15 00:22:48 +02:00
the-djmaze
daf0f089b8 Ignore text/x-amp-html https://www.caniemail.com/features/amp/ 2024-04-14 15:15:15 +02:00
the-djmaze
8a461e7a3d Speedup Knockout a bit 2024-04-14 15:14:23 +02:00
the-djmaze
24681f8955 Improved example for #1544 2024-04-14 02:16:40 +02:00
the-djmaze
fffc044998 Resolve #1543 2024-04-13 13:18:31 +02:00
the-djmaze
3ced479ac9 Example Nextcloud login override for #1544 2024-04-13 11:38:45 +02:00
the-djmaze
41fc41984a Improved MessagesPerPage valid value #1540 2024-04-08 17:27:40 +02:00
the-djmaze
bcc2c7b9ca Prevent Infinity #1540 2024-04-08 17:22:27 +02:00
the-djmaze
969dca5f7e Predis to v2.2.2 2024-04-08 16:29:55 +02:00
the-djmaze
9510347d3a DoAttachmentsActions $mResult not used 2024-04-08 16:14:56 +02:00
the-djmaze
d59a326920 Bugfix: hide pagination when search has 0 messages 2024-04-08 16:14:30 +02:00
the-djmaze
2bcaf3c6ad Prevent mbstring error before setup.php 2024-04-05 23:51:30 +02:00
the-djmaze
3988ea9ba5 cleanup unused js parameters 2024-04-03 00:15:29 +02:00
the-djmaze
ed673fdba8 Bugfix: mime parser headerValue() is null 2024-04-02 22:40:12 +02:00
the-djmaze
ad417c743e ES2020 2024-04-02 22:37:18 +02:00
the-djmaze
cab81d5214 Small code reduction in Knockout 2024-04-02 22:37:02 +02:00
the-djmaze
78178ecc2f Everything to ES2020 2024-04-02 22:24:53 +02:00
the-djmaze
84ffe1e552 Predis to v2.2.2 2024-04-02 20:15:23 +02:00
the-djmaze
f6b440adef Put available encrypt and sign options in corresponding menu options as title 2024-04-01 02:09:23 +02:00
the-djmaze
9b309c31bd fabrica('login') never used, so drop it 2024-04-01 02:08:44 +02:00
the-djmaze
8e0303f065 Resolve #1533 2024-03-31 21:06:39 +02:00
the-djmaze
a424c6a52a Extensions use \MailSo\Log\Inherit 2024-03-31 21:02:05 +02:00
the-djmaze
c4acb26758 Bugfix: extensions set logger failed 2024-03-31 20:47:43 +02:00
the-djmaze
a9e5f1c6f8 Improved comments 2024-03-31 15:32:44 +02:00
the-djmaze
b9e2d6caca Bugfix content encoding and type detection.
Reported in #1450
2024-03-31 15:16:11 +02:00
Maarten
a76564fce7
Merge pull request #1528 from hguilbert/patch-19
Update fr.json
2024-03-31 04:43:44 +02:00
Maarten
8056bf8959
Merge pull request #1529 from hguilbert/patch-20
Update fr.json
2024-03-31 04:43:36 +02:00
Maarten
c673f5fb7a
Merge pull request #1530 from hguilbert/patch-21
Update user.json
2024-03-31 04:43:29 +02:00
Maarten
a828c3e075
Merge pull request #1531 from mayswind/master
update Chinese translation
2024-03-31 04:43:22 +02:00
Maarten
a745972cd4
Merge pull request #1532 from spoooyders/localization-be-by
Update Belarusian translation.
2024-03-31 04:43:15 +02:00
pavuki
29d3010c80 Update Belarusian translation. 2024-03-30 12:36:31 +01:00
MaysWind
4ded93cc0a update Chinese translation 2024-03-30 15:26:34 +08:00
hguilbert
7c79b0ecb7
Update user.json 2024-03-29 06:30:12 +01:00
hguilbert
274fd9de77
Update fr.json 2024-03-29 06:23:15 +01:00
hguilbert
5580ada03f
Update fr.json 2024-03-29 06:20:48 +01:00
the-djmaze
2a56595cf9 Rename languages in extensions 2024-03-29 01:06:47 +01:00
Maarten
04896f5d88
Merge pull request #1527 from tinola/master
[plugins][two-factor-auth] rename language files names (rfc5646)
2024-03-29 00:49:33 +01:00
tinola
bdd9a80db1 [plugins][two-factor-auth] rename language files names (rfc5646) 2024-03-28 20:10:20 +01:00
Maarten
336f7d482e
Merge pull request #1523 from tinola/master
Update polish translation
2024-03-28 02:56:30 +01:00
Maarten
721e73101c
Merge pull request #1524 from hguilbert/patch-17
Update user.json
2024-03-28 02:56:23 +01:00
Maarten
8c9468da84
Merge pull request #1525 from hguilbert/patch-18
Update admin.json
2024-03-28 02:56:15 +01:00
the-djmaze
143050176b Resolve #1520 2024-03-28 02:44:38 +01:00
the-djmaze
3017f8c656 Simplify LoginProcess handling 2024-03-28 02:43:39 +01:00
hguilbert
987ed0fcba
Update admin.json 2024-03-27 18:34:51 +01:00
hguilbert
6fe336e66c
Update user.json 2024-03-27 18:28:37 +01:00
tinola
62f2fe4f52
Update user.json 2024-03-27 08:10:58 +01:00
tinola
47e427a892 Update polish translation 2024-03-27 08:08:42 +01:00
tinola
74229478ae Update polish translation 2024-03-27 07:57:21 +01:00
the-djmaze
6be84f8ab4 Added ConnectionSecurityType::TLS 2024-03-27 01:24:34 +01:00
the-djmaze
7213395118 Throw more errors when using PHP mail() 2024-03-27 01:24:12 +01:00
the-djmaze
35db1f4eb8 SASL OAuth rename 3rd parameter to accessToken 2024-03-27 01:23:48 +01:00
the-djmaze
b6df50546e logMask all AUTHENTICATE requests 2024-03-27 01:23:11 +01:00
the-djmaze
f745a54669 ErrorTip use white-space: pre 2024-03-27 01:22:01 +01:00
the-djmaze
652fbced3c Improved translator a bit 2024-03-26 23:30:58 +01:00
Maarten
c77edf37e1
Merge pull request #1522 from ner00/master
Update Portuguese
2024-03-26 23:29:29 +01:00
lmperfis
a70a7c4c95 Update Portuguese 2024-03-26 18:37:07 +00:00
the-djmaze
f38e5a41cd Solution for #1521 2024-03-26 19:12:50 +01:00
the-djmaze
b96cb41876 Added Autoconfig detect through DNS SRV (RFC 6186 & 8314) and disable MX 2024-03-26 18:24:57 +01:00
the-djmaze
f9048d1209 v2.36.0 2024-03-26 15:21:47 +01:00
the-djmaze
07710f685f Drop hooks service.app-delay-start-* in favour of the existing json.before-appdelaystart and json.after-appdelaystart 2024-03-26 15:04:23 +01:00
the-djmaze
57fbdf8795 Added: allow setting the supported THREAD algorithm 2024-03-26 15:01:38 +01:00
the-djmaze
015dcbdc41 Bugfix: error undefined msg.from[0] 2024-03-26 12:26:53 +01:00
the-djmaze
c14d7bdd46 Add some comments and some renames 2024-03-26 12:26:35 +01:00
the-djmaze
25214f2d87 Improved Domain whitelist handling 2024-03-26 04:08:26 +01:00
the-djmaze
7f94c26bee Cache ThreadsOldUids based on bThreadSort true/false #1507 2024-03-25 19:53:04 +01:00
the-djmaze
c1e25dedb9 Cleanup SMTP auth handling 2024-03-25 19:36:39 +01:00
the-djmaze
55642e826e Resolve #1517 2024-03-25 19:34:30 +01:00
the-djmaze
31d929912a Improved SASL handling 2024-03-25 19:31:57 +01:00
the-djmaze
40622d45b7 More changes for #1507 2024-03-25 14:17:49 +01:00
the-djmaze
76b6f32222 Bugfix: GetUids() returned wrong ids when using SequenceSet 2024-03-25 12:54:09 +01:00
the-djmaze
d0ee5325f4 Changed MessageListThreadsMap(MessageCollection...) to MessageListThreadsMap(FolderInformation...) 2024-03-25 10:44:25 +01:00
the-djmaze
93a4d5ba88 Fix to use the correct $oParams->hash() 2024-03-25 10:43:51 +01:00
the-djmaze
8bcd0a3521 Sorted languages by name 2024-03-25 02:38:11 +01:00
the-djmaze
248dcb8257 Most languages to ISO 639-1 2024-03-25 02:20:18 +01:00
the-djmaze
fdc2d5a490 Attempt to solve #1507 2024-03-25 01:43:22 +01:00
the-djmaze
c34f99dd6e Make sure etag used on MessageList for #1515 2024-03-25 01:42:49 +01:00
the-djmaze
43622b50dc Bugfix PHP Warning: Undefined array key 2024-03-24 20:30:26 +01:00
the-djmaze
759933e24b Also check language helper after shortening 2024-03-24 19:19:54 +01:00
the-djmaze
59a02af2b7 Languages use rfc5646, by using the shortest ISO 639 code by default 2024-03-24 19:13:47 +01:00
Maarten
d65a88305c
Merge pull request #1512 from spoooyders/localization-be-by
Add Belarusian localization.
2024-03-24 18:33:37 +01:00
the-djmaze
2a116612be Return false on more server issues regarding domains control #1513 2024-03-24 17:22:57 +01:00
the-djmaze
6df5802f97 Check if saving disabled domains works #1513 2024-03-24 17:10:23 +01:00
the-djmaze
b3c10190cb Moved application.ini sieve_auth_plain_initial to per domain config 2024-03-24 16:49:00 +01:00
the-djmaze
3513669412 Resolve #1514 2024-03-24 15:03:50 +01:00
the-djmaze
b45459d645 Sieve always allow fileinto INBOX #1510 2024-03-24 14:18:08 +01:00
the-djmaze
60de50da64 Resolve #1513 broken old RainLoop .ini parser 2024-03-24 13:13:55 +01:00
pavuki
98a45e383b Fix belarusian localization. 2024-03-23 13:32:36 +01:00
pavuki
b46f5c9ec2 Fix belarusian localization. 2024-03-23 13:30:06 +01:00
pavuki
c4e98c8b4c Add Belarusian localization. 2024-03-23 09:46:55 +01:00
the-djmaze
4fc04648cf Moved cache drivers outside core to extensions (plugins) 2024-03-22 04:03:39 +01:00
the-djmaze
d5690fc579 Set admin translation to GLOBAL/PASSWORD 2024-03-22 02:47:40 +01:00
the-djmaze
068922f05f Support .swapColors in NC25+ theme 2024-03-22 02:37:06 +01:00
the-djmaze
705822fe7a Log debug message for failing custom themes directory #1508 2024-03-22 02:36:41 +01:00
the-djmaze
f0cf39e297 Resolve #1509 2024-03-22 02:19:19 +01:00
the-djmaze
8c077a94c6 same small fixes 2024-03-20 00:45:11 +01:00
the-djmaze
6bd66b8a2d Idea for #1507 2024-03-19 20:09:30 +01:00
the-djmaze
a08d376023 Added: Search in HEADER. Like: "header=X-Rspamd-Action no action" 2024-03-19 19:59:33 +01:00
the-djmaze
94aa2f11c4 Rename MessageSimple* commands to Message* 2024-03-19 19:49:11 +01:00
the-djmaze
173a172cf8 Resolve #485 2024-03-19 17:31:32 +01:00
the-djmaze
d59563cf1b Removed unused ClientException 2024-03-19 17:30:20 +01:00
the-djmaze
386fd936b4 Slight code reduction for #419 2024-03-19 15:33:52 +01:00
the-djmaze
7450afeecf Replace all new Intl.Collator with new baseCollator 2024-03-19 15:24:34 +01:00
the-djmaze
70b1a93108 CompactComposer support older browsers and a different idea to modify the dialog for #1498 2024-03-19 14:10:03 +01:00
the-djmaze
0d34d0b25b Merge branch 'master' of github.com:the-djmaze/snappymail 2024-03-19 13:09:39 +01:00
the-djmaze
d2a99ad7f2 bugfix: compose window closing tags should be TD 2024-03-19 13:09:23 +01:00
Maarten
0aadf987ac
Merge pull request #1506 from hguilbert/patch-16
Update user.json
2024-03-19 11:53:05 +01:00
hguilbert
dfa3ffecd0
Update user.json 2024-03-19 06:19:09 +01:00
the-djmaze
f36c9ab7d6 Bugfix: download all attachments icon 2024-03-19 03:16:09 +01:00
the-djmaze
dfdfeb2641 viewDkimIcon to dkimIcon 2024-03-19 03:11:01 +01:00
the-djmaze
1db3952ea4 Remove unused font glyph "mobile phone" 2024-03-19 02:43:41 +01:00
the-djmaze
abaf5a96fa Bugfix: sort accounts drag & drop 2024-03-18 20:30:04 +01:00
the-djmaze
165ac87685 Translate dutch 2024-03-18 19:43:20 +01:00
the-djmaze
cc03185ea0 Added icon to system folders 2024-03-18 19:16:12 +01:00
the-djmaze
eaf405e250 fix handling displaySpecSetting in edit folder popup 2024-03-18 17:53:15 +01:00
the-djmaze
c2c72dc4be Let PECL::isSupported() return false instead 2024-03-18 17:51:56 +01:00
the-djmaze
b98b21c1c6 S/MIME allow changing private key passphrase due to #1505 2024-03-18 17:17:25 +01:00
the-djmaze
570c7e1a1a Some minor backup example extension changes for #1042 2024-03-18 16:28:54 +01:00
the-djmaze
190db75c19 Remove remembered passphrase after 15 minutes of inactivity
https://github.com/the-djmaze/snappymail/issues/1142#issuecomment-1635839009
2024-03-18 14:36:33 +01:00
1312 changed files with 58311 additions and 21890 deletions

View file

@ -1,7 +1,7 @@
# syntax=docker/dockerfile:1
FROM alpine:3.18.5 AS builder
RUN apk add --no-cache php82 php82-json php-phar php-zip
FROM alpine:3.24.3 AS builder
RUN apk add --no-cache php85 php85-json php-phar php-zip
RUN apk add --no-cache npm
RUN npm install -g gulp yarn
WORKDIR /source
@ -23,7 +23,7 @@ RUN set -eux; \
rm -v /snappymail/README.md /snappymail/_include.php
# Inspired by the original Rainloop dockerfile from youtous on GitLab
FROM php:8.2-fpm-alpine AS final
FROM php:8.5-fpm-alpine AS final
LABEL org.label-schema.description="SnappyMail webmail client image using nginx, php-fpm on Alpine"
@ -99,9 +99,6 @@ RUN set -eux; \
# mysql
RUN docker-php-ext-install pdo_mysql
# opcache
RUN docker-php-ext-install opcache
# postgres
RUN set -eux; \
apk add --no-cache postgresql-libs; \
@ -122,6 +119,13 @@ RUN set -eux; \
apk del .build-dependencies; \
apk del .deps
# sqlite
RUN set -eux; \
apk add --no-cache sqlite-libs; \
apk add --no-cache --virtual .deps sqlite-dev; \
docker-php-ext-install pdo_sqlite; \
apk del .deps
# tidy
RUN set -eux; \
apk add --no-cache tidyhtml; \

View file

@ -9,6 +9,12 @@ UPLOAD_MAX_SIZE=${UPLOAD_MAX_SIZE:-25M}
MEMORY_LIMIT=${MEMORY_LIMIT:-128M}
SECURE_COOKIES=${SECURE_COOKIES:-true}
# Disable listening on ipv6 for ipv4-only environments, or nginx will fail to start
if ! ip a | grep 'inet6 ' > /dev/null; then
echo "[INFO] ipv6 is disabled. Configuring nginx to not listen on ipv6"
sed -i '/listen \[::\]:/d' /etc/nginx/nginx.conf
fi
# Set attachment size limit
sed -i "s/<UPLOAD_MAX_SIZE>/$UPLOAD_MAX_SIZE/g" /usr/local/etc/php-fpm.d/php-fpm.conf /etc/nginx/nginx.conf
sed -i "s/<MEMORY_LIMIT>/$MEMORY_LIMIT/g" /usr/local/etc/php-fpm.d/php-fpm.conf

View file

@ -52,6 +52,8 @@ http {
server {
listen 8888;
listen [::]:8888;
#listen [::]:8888 ipv6only=off;
root /snappymail;
index index.php;
charset utf-8;

View file

@ -3,7 +3,7 @@ module.exports = {
// extends: ['eslint:recommended', 'plugin:prettier/recommended'],
extends: ['eslint:recommended'],
parserOptions: {
ecmaVersion: 6,
ecmaVersion: 11,
sourceType: 'module'
},
env: {
@ -37,7 +37,11 @@ module.exports = {
// Mailvelope
'mailvelope': "readonly",
// Punycode
'IDN': "readonly"
'IDN': "readonly",
// Turndown
'TurndownService': "readonly",
// Marked
'marked': "readonly"
},
// http://eslint.org/docs/rules/
rules: {

View file

@ -30,8 +30,9 @@ If applicable, add screenshots to help explain your problem.
- SnappyMail Version:
- Mode: [e.g. standalone, nextcloud, cyberpanel, docker]
**[Debug/logging information](https://github.com/the-djmaze/snappymail/wiki/FAQ#how-do-i-enable-logging)**
Place them here (few lines) or as attachments (many lines)
**Debug/logging information**
[Read here how to log](https://github.com/the-djmaze/snappymail/wiki/FAQ#how-do-i-enable-logging)
- [ ] I've placed them here (few lines) or as attachments (many lines)
**Additional context**
Add any other context about the problem here.

2
.gitignore vendored
View file

@ -25,6 +25,8 @@
/vendors/knockout/spec
/vendors/openpgp-5
!/vendors/openpgp-5/dist
/vendors/squire2
!/vendors/squire2/dist
/vendors/vanillaqr.js/
/integrations/nextcloud/rainloop
/integrations/owncloud/rainloop

4
.gitmodules vendored
View file

@ -1,3 +1,7 @@
[submodule "vendors/openpgp-5"]
path = vendors/openpgp-5
url = git@github.com:the-djmaze/openpgpjs.git
[submodule "vendors/squire2"]
path = vendors/squire2
url = git@github.com:the-djmaze/Squire.git
branch = snappymail

View file

@ -1,7 +1,15 @@
<FilesMatch "index\.php">
AcceptPathInfo On
</FilesMatch>
<IfModule mod_rewrite.c>
RewriteEngine On
# Redirect cPanel
RewriteRule cpsess.* https://%{HTTP_HOST}/ [L,R=301]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.+)$ index.php/$1 [L,QSA]
</IfModule>
<IfModule mod_expires.c>

View file

@ -1,3 +1,380 @@
## 2.38.2 – 2024-10-09
### Fixed
- error '$index is not defined' in Settings > Accounts
[#1797](https://github.com/the-djmaze/snappymail/issues/1797)
## 2.38.1 – 2024-10-08
### Added
- Admin - Extensions search filter
- Options to unset current font family and size
[#1726](https://github.com/the-djmaze/snappymail/issues/1726)
- Option to allow insecure cryptkey
[#1746](https://github.com/the-djmaze/snappymail/issues/1746)
- Save vCard FN property
[#1761](https://github.com/the-djmaze/snappymail/issues/1761)
- Docker Nginx should listen on IPv6 addresses in addition to IPv4
[#1770](https://github.com/the-djmaze/snappymail/issues/1770)
- Full-screen Message View on Double-Click
[#1787](https://github.com/the-djmaze/snappymail/issues/1787)
### Changed
- Use the custom Squire as submodule vendors/squire2
- Disallow noembed and noframes HTML elements
- Keep 1 space between HTML elements in compressed templates
- Squire: sanitizeToDOMFragment now uses cleanHTML()
- Squire: improved handling of BR elements
[#1389](https://github.com/the-djmaze/snappymail/issues/1389)
- Nextcloud border-radius by @gnilebein
[#1790](https://github.com/the-djmaze/snappymail/pull/1790)
- Compose window sentFolder handling for
[#1793](https://github.com/the-djmaze/snappymail/pull/1793)
- Update Portuguese by @ner00
- Update Polish by @tinola
- Update French by @hguilbert
- Update Portuguese (Brazil) by @mstolf
### Fixed
- Composer dialog scroll got broken in v2.28
- Composer dialog "from" triangle button wrong position due to font changes
- Admin - Config `search` should be ko.observable() not ko.observableArray()
- Squire: paste images
[#1389](https://github.com/the-djmaze/snappymail/issues/1389)
- Domain whitelist failures on login
[#1706](https://github.com/the-djmaze/snappymail/issues/1706)
- Sieve parse errors
- Sieve support `index` was not optional for
[#1709](https://github.com/the-djmaze/snappymail/issues/1709)
- Pagination problem for large mailbox after 2.36.1
[#1716](https://github.com/the-djmaze/snappymail/issues/1716)
- Undefined constant "LOG_ERROR"
[#1754](https://github.com/the-djmaze/snappymail/issues/1754)
- Search Filter Capital "B" not working
[#1780](https://github.com/the-djmaze/snappymail/issues/1780)
- PHP 8.4: Implicitly nullable parameter declarations deprecated
## 2.38.0 – 2024-09-16
### Added
- Remove `tel:` links when converting HTML to plain
[#1724](https://github.com/the-djmaze/snappymail/issues/1724)
- Convert HTML to Markdown instead of plain, by using [Turndown](https://github.com/mixmark-io/turndown)
[#1604](https://github.com/the-djmaze/snappymail/issues/1604)
- Check HTMLInputElement.validity() for
[#1733](https://github.com/the-djmaze/snappymail/issues/1733)
### Changed
- Use a modified [Squire 2.3.2](https://github.com/the-djmaze/Squire/commits/snappymail/)
- cleanHtml use allowedTags instead of disallowedTags and improved CSS handling
- Update Portuguese by @ner00
### Fixed
- mXSS exploit found by SonarSource
[CVE-2024-45800](https://github.com/the-djmaze/snappymail/security/advisories/GHSA-2rq7-79vp-ffxm)
- Call to a member function Email() on null
[#1706](https://github.com/the-djmaze/snappymail/issues/1706)
- IMAP capabilities via IMAP Proxy
[#1725](https://github.com/the-djmaze/snappymail/issues/1725)
- Messages on page setting is not validated against 999 max
[#1733](https://github.com/the-djmaze/snappymail/issues/1733)
## 2.37.3 – 2024-08-27
### Added
- Mark images with width=1 as tracking pixel
- Show warning in Admin -> About when PHP runs in 32bit
- Edit ACL rules
[#157](https://github.com/the-djmaze/snappymail/issues/157)
- Show GnuPG version for
[#1560](https://github.com/the-djmaze/snappymail/issues/1560)
- Make sure only scalar values are allowed in $_ENV for
[#1560](https://github.com/the-djmaze/snappymail/issues/1560)
- Change minimum new mail check interval
[#1678](https://github.com/the-djmaze/snappymail/issues/1678)
- Sieve editor does not support "index" extension
[#1709](https://github.com/the-djmaze/snappymail/issues/1709)
### Changed
- Improved domain autoconfig interaction
- MS autodiscover priorities DNS over subdomain
- Simplify sieve scripts list
[#1675](https://github.com/the-djmaze/snappymail/issues/1675)
- Handling of (token) errors due to
[#1706](https://github.com/the-djmaze/snappymail/issues/1706)
- Sabre/Xml to v4.0.5
- Update Chinese by @Artoria2e5
- Update French by @hguilbert
### Fixed
- Thread sorting visible after disabling the imap capability
[#1574](https://github.com/the-djmaze/snappymail/issues/1574)
- Creating new message impossible as long as a draft exists?
[#1710](https://github.com/the-djmaze/snappymail/issues/1710)
- InvalidToken error at login
[#1706](https://github.com/the-djmaze/snappymail/issues/1706)
### Nextcloud
- Force Nextcloud personal language by default
[#1428](https://github.com/the-djmaze/snappymail/issues/1428)
## 2.37.2 – 2024-08-13
### Added
- Validate Fetch JSON response
### Fixed
- PATH_INFO bug due to Office365 OAuth login
- Prevent logout loop on error
### Nextcloud
- Failed loading due to Office365 OAuth2 attempt
[#1703](https://github.com/the-djmaze/snappymail/issues/1703)
## 2.37.1 – 2024-08-12
### Fixed
- Gulp v5 broke the fonts
### Nextcloud
- Support v30
## 2.37.0 – 2024-08-12
### Added
- JavaScript event `rl-vm-visible`
- Detailed error message on account switch failure for
[#1594](https://github.com/the-djmaze/snappymail/issues/1594)
- Workarounds for Microsoft OAuth2 (currently requires Apache AcceptPathInfo)
[#1645](https://github.com/the-djmaze/snappymail/issues/1645)
- Support "mark for deletion"
[#1657](https://github.com/the-djmaze/snappymail/issues/1657) by @smsoft-ru
- Invoke "Update Identity" pop up right after login (when not initialized)
[#1689](https://github.com/the-djmaze/snappymail/issues/1689)
- Keyboard shortcut for "Swap default (background) color"
[#1690](https://github.com/the-djmaze/snappymail/issues/1690)
### Changed
- Updated gulp to v5
- Replaced vulnerable gulp-header with gulp-append-prepend
- Removed abandoned vulnerable rollup-plugin-html
- Align save button in admin security settings
- Made registerProtocolHandler('mailto') optional by activating at Settings -> General
- Improved InvalidToken handling for
[#1653](https://github.com/the-djmaze/snappymail/issues/1653)
- Cleanup localizations
- Update French by @hguilbert
- Update German by @tkasch
- Update Polish by @tinola
- Update Portuguese by @ner00
### Fixed
- prevent multiple afterShow() and afterHide() due to `transitionend` on multiple CSS properties
- Attempt to read property "smimeSigned" on null
- Refreshing mail list doesn't update current message
[#1654](https://github.com/the-djmaze/snappymail/issues/1654)
- Deletion of CACHE folder causing error
[#1660](https://github.com/the-djmaze/snappymail/issues/1660)
- Multiple line breaks are not displayed
[#1666](https://github.com/the-djmaze/snappymail/issues/1666)
- RainLoop\Exceptions\ClientException::__construct(): Argument #2 ($oPrevious) must be of type ?Throwable, string given
[#1686](https://github.com/the-djmaze/snappymail/issues/1686)
- SpamAssassin Division by zero
[#1694](https://github.com/the-djmaze/snappymail/issues/1694)
- Failed to parse RFC 2822 date '6 Jul 2024 16:42:09 +0200'
[#1694](https://github.com/the-djmaze/snappymail/issues/1694)
- Fix capabilities when THREAD is disabled
[#1698](https://github.com/the-djmaze/snappymail/pull/1698) by @akhil1508
### Nextcloud
- Failed loading due to incorrect `app_path`
- Bugfix language detection
- Allow multi-account in nc with oauth login
[#1699](https://github.com/the-djmaze/snappymail/pull/1699) by @akhil1508
## 2.36.4 – 2024-06-25
### Added
- Customize private key passphrase expiration interval
[#1545](https://github.com/the-djmaze/snappymail/discussions/1545)
- AdvancedSearch support for filtering mails before a given date
[#1606](https://github.com/the-djmaze/snappymail/pull/1606) by @codiflow
- Control valid spam and virus headers
[#1607](https://github.com/the-djmaze/snappymail/issues/1607)
- Remember S/MIME private Key without function
[#1611](https://github.com/the-djmaze/snappymail/issues/1611)
- Resize compose dialog
- Magnetic theme
[#1637](https://github.com/the-djmaze/snappymail/pull/1637) by @TheCuteFoxxy
### Changed
- Improved signing messages by allowing to choose between the options
- Improved language detection code
- More detailed Decrypt errors
- Update French by @hguilbert
- Update Polish by @tinola
- Update Portuguese by @ner00
- Update Spanish by @huloza
### Fixed
- Default language error
- Undefined $sEmail in DoAdminDomainMatch
- Handling Autocrypt header failed on `=`
[#1608](https://github.com/the-djmaze/snappymail/issues/1608)
- Blank lines are inserted when editing draft
[#1609](https://github.com/the-djmaze/snappymail/issues/1609)
- Workaround Cyrus MAILBOXID bug (disable OBJECTID capability by default due to impact)
[#1640](https://github.com/the-djmaze/snappymail/issues/1640)
- Workaround HTML with multiple body elements or MIME with multiple text/html
[#1641](https://github.com/the-djmaze/snappymail/issues/1641)
### Nextcloud
- OIDC stay logged in
[#1620](https://github.com/the-djmaze/snappymail/pull/1620) by @avinash-0007
## 2.36.3 – 2024-05-27
### Changed
- UserAuth prevent plugin errors (like the Nextcloud plugin did)
### Fixed
- Undefined variable $aTokenData
[#1567](https://github.com/the-djmaze/snappymail/issues/1567)
## 2.36.2 – 2024-05-26
### Added
- "copy to" action in menu's for
[#1559](https://github.com/the-djmaze/snappymail/issues/1559)
- Log signal info for
[#1569](https://github.com/the-djmaze/snappymail/issues/1569)
- OpenPGP.js automatically import backup keys from server
### Changed
- Improved "remember me" cookie handling
- Update Basque by @Thadah
- Update Portuguese by @ner00
### Fixed
- "Account already exists"
[#1561](https://github.com/the-djmaze/snappymail/issues/1561)
- Properly escape path separator in tar.php file list regex
[#1562](https://github.com/the-djmaze/snappymail/pull/1562) by @sevmonster
- Prevent mkdir() error
[#1565](https://github.com/the-djmaze/snappymail/issues/1565)
- SCRAM Exception when trying to log in to SMTP
[#1575](https://github.com/the-djmaze/snappymail/issues/1575)
- Error when redirected back to instance after Gmail OAuth
[#1580](https://github.com/the-djmaze/snappymail/issues/1580)
- Uncaught TypeError: hasPublicKeyForEmails(...).then is not a function
[#1589](https://github.com/the-djmaze/snappymail/issues/1589)
- Undefined variable $sFilename
- GPG/PGP exec() return false handling
### Nextcloud
- OIDC login active again
[#1572](https://github.com/the-djmaze/snappymail/pull/1572) by @avinash-0007
## 2.36.1 – 2024-04-23
### Added
- Autoconfig detect through DNS SRV (RFC 6186 & 8314) and disable MX
- Have I Been Pwned class to check breached passwords and email addresses
- Handle RFC 5987 in Content-Disposition header
- Ignore text/x-amp-html
- Show SMTP error to user
[#1521](https://github.com/the-djmaze/snappymail/issues/1521)
- OAuth2 for login using gmail (and others)
### Changed
- logMask all AUTHENTICATE requests
- ErrorTip use white-space: pre
- Simplify LoginProcess handling
- ES2020 everywhere (require Safari 13.1)
- Modified Squire to be more in line with v2.2.8
- CSS set min-width for .attachmentParent and .flagParent to line them up
- cPanel use extension login-cpanel instead of login-remote
- Improved login credentials handling
- Speedup Knockout a bit and removed `with($context)` scope
- Update Belarusian by @spoooyders
- Update Chinese by @mayswind
- Update French by @hguilbert
- Update Polish by @tinola
- Update Portuguese by @ner00
### Fixed
- Content encoding and type detection in JavaScript could fail due to case-sensitivity.
- Extensions set logger failed
- GnuPG check open_basedir and if shell_exec is disabled
[#1385](https://github.com/the-djmaze/snappymail/issues/1385)
[#1496](https://github.com/the-djmaze/snappymail/issues/1496)
[#1555](https://github.com/the-djmaze/snappymail/issues/1555)
- Hide pagination when search result has no messages
- Prevent mbstring error before setup.php
- Prevent MessagesPerPage Infinity
[#1540](https://github.com/the-djmaze/snappymail/issues/1540)
- Reseal CryptKey failed
[#1543](https://github.com/the-djmaze/snappymail/issues/1543)
### Nextcloud
- Add an occ command to set up the login settings
[#1552](https://github.com/the-djmaze/snappymail/issues/1552)
## 2.36.0 – 2024-03-18
### Added
- Allow setting the supported THREAD algorithm
- Icon to system folders
- Remove remembered password after 15 minutes of inactivity
[#1142](https://github.com/the-djmaze/snappymail/issues/1142)
- Swap background and text color for unreadable text on dark background
[#1486](https://github.com/the-djmaze/snappymail/issues/1486)
- Generate TOTP code at ?Admin -> Security
[#1501](https://github.com/the-djmaze/snappymail/issues/1501)
- Button to change S/MIME private key passphrase
[#1505](https://github.com/the-djmaze/snappymail/issues/1505)
- Belarusian
[#1512](https://github.com/the-djmaze/snappymail/pull/1512) by @spoooyders
- Log some domain idn_to_ascii issues
[#1513](https://github.com/the-djmaze/snappymail/issues/1513)
### Changed
- On folder/mailbox rename, also rename all children instead of reloading all
- Seal MainAccount CryptKey and on error ask old login passphrase to reseal key.
- Moved cache drivers outside core into extensions
- Sieve always allow fileinto INBOX
[#1510](https://github.com/the-djmaze/snappymail/issues/1510)
- Moved application.ini `sieve_auth_plain_initial` to per domain config
- Languages use rfc5646, by using the shortest ISO 639 code by default
- Update French by @hguilbert
- Update Portuguese by @ner00
### Fixed
- On folder/mailbox rename, the old fullName must be removed from cache
- On folder/mailbox rename, the checkable option was not renamed
- Sort accounts drag & drop
- S/MIME encrypted and opaque signed not visible
[#1450](https://github.com/the-djmaze/snappymail/issues/1450)
- Wrong last UID of thread
[#1507](https://github.com/the-djmaze/snappymail/issues/1507)
- Creation of dynamic property SnappyMail\DAV\Client::$HTTP
[#1509](https://github.com/the-djmaze/snappymail/issues/1509)
- "Download as ZIP" fails for messages
[#1514](https://github.com/the-djmaze/snappymail/issues/1514)
- SMTP "Authentication failed" when IMAP uses `shortLogin` and SMTP not
[#1517](https://github.com/the-djmaze/snappymail/issues/1517)
## 2.35.4 – 2024-03-16
### Added
@ -6,7 +383,6 @@
### Changed
- OpenPGP.js to v5.11.1
- punycode.js lowercase domain names
- Nextcloud changed stored password handling
- application.ini `login_lowercase` removed and now configurable per domain JSON `lowerLogin`
- Update Portuguese by @ner00
@ -21,6 +397,11 @@
[#1500](https://github.com/the-djmaze/snappymail/issues/1500)
- VCard PHP Notice: Undefined index: ENCODING
### Nextcloud
- Changed stored password handling
- Can't login from nextcloud with 2.35.3 bug Nextcloud
[#1490](https://github.com/the-djmaze/snappymail/issues/1490)
## 2.35.3 – 2024-03-12
@ -68,8 +449,9 @@
- Crash on importing corrupt OpenPGP keys
- Crash on old browsers instead of showing error
- Ignore popups on logoutReload()
[#1467](https://github.com/the-djmaze/snappymail/issues/1467)
- Custom SASLMechanisms fail in IMAP when the connection is secure
[#1484](https://github.com/the-djmaze/snappymail/pull/1484)
[#1484](https://github.com/the-djmaze/snappymail/pull/1484) by @botsarenthuman
## 2.35.2 – 2024-02-27
@ -86,7 +468,9 @@
- Drop support for gnupg PECL extension as it fails with "no passphrase" issues
- Error 352 when getting Filters
[#1455](https://github.com/the-djmaze/snappymail/issues/1455)
- Nextcloud SetPassword(): Argument #1 must be of type SensitiveString, string given
### Nextcloud
- SetPassword(): Argument #1 must be of type SensitiveString, string given
[#1456](https://github.com/the-djmaze/snappymail/issues/1456)
@ -267,8 +651,8 @@
- Run full GetUids() in background when message_list_limit is set
- MessageListThreadsMap as background task when message_list_limit is set
- Properly set CACHEDIR.TAG
- Sending group email to all contact addresses by @rezaei92
[#1286](https://github.com/the-djmaze/snappymail/pull/1286)
- Sending group email to all contact addresses
[#1286](https://github.com/the-djmaze/snappymail/pull/1286) by @rezaei92
### Changed
- Default IMAP message_list_limit to 10000
@ -276,8 +660,8 @@
- Improved Squire WYSIWYG
- Sort real attachments and inline attachments for
[#1360](https://github.com/the-djmaze/snappymail/issues/1360)
- Nextcloud Theme fixes and improvements by @hampoelz
[#1363](https://github.com/the-djmaze/snappymail/pull/1363)
- Nextcloud Theme fixes and improvements
[#1363](https://github.com/the-djmaze/snappymail/pull/1363) by @hampoelz
- Improve display of attachments
[#1361](https://github.com/the-djmaze/snappymail/issues/1361)
- Rename messageVisibility to messageVisible
@ -291,8 +675,8 @@
- Preview tooltip shows "null" when PREVIEW capability is disabled
### Nextcloud
- Improved language handling by @avinash-0007
[#1362](https://github.com/the-djmaze/snappymail/pull/1362)
- Improved language handling
[#1362](https://github.com/the-djmaze/snappymail/pull/1362) by @avinash-0007
- FilterLanguage had wrong parameter order
- Use NextcloudV25+ theme by default
@ -313,8 +697,8 @@
[#1344](https://github.com/the-djmaze/snappymail/issues/1344)
- NO Mailbox does not exist, or must be subscribed to")
[#1354](https://github.com/the-djmaze/snappymail/issues/1354)
- Flag indicators are added to wrong message by @SergeyMosin
[#1347](https://github.com/the-djmaze/snappymail/pull/1347)
- Flag indicators are added to wrong message
[#1347](https://github.com/the-djmaze/snappymail/pull/1347) by @SergeyMosin
- Squire: issue when using the enter key in a reply window
[#1296](https://github.com/the-djmaze/snappymail/issues/1296)
- Squire: crash on cut/delete range
@ -428,8 +812,8 @@
[#1308](https://github.com/the-djmaze/snappymail/issues/1308)
- With email open, "delete" doesn't delete #1274
[#1274](https://github.com/the-djmaze/snappymail/issues/1274)
- Fix threading view in Thunderbird (others?) by @tkasch
[#1304](https://github.com/the-djmaze/snappymail/pull/1304)
- Fix threading view in Thunderbird (others?)
[#1304](https://github.com/the-djmaze/snappymail/pull/1304) by @tkasch
## 2.29.1 – 2023-10-02
@ -443,18 +827,18 @@
### Added
- Modern UI / Nextcloud Theme
[#629](https://github.com/the-djmaze/snappymail/pull/629) by @hampoelz
- "Add/Edit signature" label to PopupsIdentity.html by @SergeyMosin
[#1248](https://github.com/the-djmaze/snappymail/pull/1248)
- "Add/Edit signature" label to PopupsIdentity.html
[#1248](https://github.com/the-djmaze/snappymail/pull/1248) by @SergeyMosin
- use calendar icon in message list for messages with '.ics' or 'text/calendar' attachments by @SergeyMosin
[#1248](https://github.com/the-djmaze/snappymail/pull/1248)
- Show unseen message count when the message list is threaded by @SergeyMosin
[#1248](https://github.com/the-djmaze/snappymail/pull/1248)
- in mobile mode hide folders(left) panel when a folder is clicked by @SergeyMosin
[#1248](https://github.com/the-djmaze/snappymail/pull/1248)
- spellcheck the subject when 'allowSpellcheck' setting is true by @SergeyMosin
[#1248](https://github.com/the-djmaze/snappymail/pull/1248)
- 'collapse_blockquotes', 'allow_spellcheck' and 'mail_list_grouped' to admin settings ('defaults' section) by @SergeyMosin
[#1248](https://github.com/the-djmaze/snappymail/pull/1248)
- Show unseen message count when the message list is threaded
[#1248](https://github.com/the-djmaze/snappymail/pull/1248) by @SergeyMosin
- in mobile mode hide folders(left) panel when a folder is clicked
[#1248](https://github.com/the-djmaze/snappymail/pull/1248) by @SergeyMosin
- spellcheck the subject when 'allowSpellcheck' setting is true
[#1248](https://github.com/the-djmaze/snappymail/pull/1248) by @SergeyMosin
- 'collapse_blockquotes', 'allow_spellcheck' and 'mail_list_grouped' to admin settings ('defaults' section)
[#1248](https://github.com/the-djmaze/snappymail/pull/1248) by @SergeyMosin
- Browser support for autocompleting TOTP code
[#1251](https://github.com/the-djmaze/snappymail/issues/1251)
@ -465,10 +849,10 @@
[#1199](https://github.com/the-djmaze/snappymail/issues/1199)
- Draft code to improve mobile breakpoints
[#1150](https://github.com/the-djmaze/snappymail/issues/1150)
- address input: space character can trigger '_parseValue' if the email address looks complete by @SergeyMosin
[#1248](https://github.com/the-djmaze/snappymail/pull/1248)
- if applicable set '\\answered' or '$forwarded' flag after a message is sent so the proper icon is shown in the message list view by @SergeyMosin
[#1248](https://github.com/the-djmaze/snappymail/pull/1248)
- address input: space character can trigger '_parseValue' if the email address looks complete
[#1248](https://github.com/the-djmaze/snappymail/pull/1248) by @SergeyMosin
- if applicable set '\\answered' or '$forwarded' flag after a message is sent so the proper icon is shown in the message list view
[#1248](https://github.com/the-djmaze/snappymail/pull/1248) by @SergeyMosin
### Fixed
- CHARSET is not valid in UTF8 mode
@ -481,39 +865,39 @@
[#1220](https://github.com/the-djmaze/snappymail/issues/1220)
- "Request failed" on message move
[#1220](https://github.com/the-djmaze/snappymail/issues/1220)
- Unwrapped text nodes attached to squire._root by @SergeyMosin
[#1234](https://github.com/the-djmaze/snappymail/pull/1234)
- Unwrapped text nodes attached to squire._root
[#1234](https://github.com/the-djmaze/snappymail/pull/1234) by @SergeyMosin
- Extra wrapper div is added in Squire every time a Draft is open (or closed) after save.
[#1208](https://github.com/the-djmaze/snappymail/issues/1208)
- foreach() argument must be of type array|object
[#1237](https://github.com/the-djmaze/snappymail/issues/1237)
- `<font>` tag 'style' is lost in replies by @SergeyMosin
[#1248](https://github.com/the-djmaze/snappymail/pull/1248)
- unseen indicator is not shown in thread view when 'listGrouped' settings is false by @SergeyMosin
[#1248](https://github.com/the-djmaze/snappymail/pull/1248)
- `<font>` tag 'style' is lost in replies
[#1248](https://github.com/the-djmaze/snappymail/pull/1248) by @SergeyMosin
- unseen indicator is not shown in thread view when 'listGrouped' settings is false
[#1248](https://github.com/the-djmaze/snappymail/pull/1248) by @SergeyMosin
- TOTP plugin is dependent on ctype
[#1250](https://github.com/the-djmaze/snappymail/issues/1250)
### Nextcloud
- iFrame mode: click on unified search result opens inner iFrame by @SergeyMosin
[#1248](https://github.com/the-djmaze/snappymail/pull/1248)
- set 'smremember' cookie if 'sign_me_auto' is set to 'DefaultOn' when using 'snappymail-autologin*', otherwise nextcloud users need to re-login when the browser is re-opened. by @SergeyMosin
[#1248](https://github.com/the-djmaze/snappymail/pull/1248)
- Improve UX of "Put in Calendar" option in plugin by @theronakpatel
[#1259](https://github.com/the-djmaze/snappymail/pull/1259)
- iFrame mode: click on unified search result opens inner iFrame
[#1248](https://github.com/the-djmaze/snappymail/pull/1248) by @SergeyMosin
- set 'smremember' cookie if 'sign_me_auto' is set to 'DefaultOn' when using 'snappymail-autologin*', otherwise nextcloud users need to re-login when the browser is re-opened
[#1248](https://github.com/the-djmaze/snappymail/pull/1248) by @SergeyMosin
- Improve UX of "Put in Calendar" option in plugin
[#1259](https://github.com/the-djmaze/snappymail/pull/1259) by @theronakpatel
## 2.28.4 – 2023-07-10
### Added
- application.ini msg_default_action by @SergeyMosin
[#1204](https://github.com/the-djmaze/snappymail/pull/1204)
- application.ini view_show_next_message by @SergeyMosin
[#1204](https://github.com/the-djmaze/snappymail/pull/1204)
- application.ini view_images by @SergeyMosin
[#1204](https://github.com/the-djmaze/snappymail/pull/1204)
- nextcloud add ability to include custom php file in InstallStep migration by @SergeyMosin
[#1197](https://github.com/the-djmaze/snappymail/pull/1197)
- application.ini msg_default_action
[#1204](https://github.com/the-djmaze/snappymail/pull/1204) by @SergeyMosin
- application.ini view_show_next_message
[#1204](https://github.com/the-djmaze/snappymail/pull/1204) by @SergeyMosin
- application.ini view_images
[#1204](https://github.com/the-djmaze/snappymail/pull/1204) by @SergeyMosin
- nextcloud add ability to include custom php file in InstallStep migration
[#1197](https://github.com/the-djmaze/snappymail/pull/1197) by @SergeyMosin
- Support plugin for Squire editor
[#1192](https://github.com/the-djmaze/snappymail/issues/1192)
@ -521,14 +905,14 @@
- only show 'Add "domain.tld" as an application for mailto links?' message after login (firefox shows the message on every reload otherwise).
[#1204](https://github.com/the-djmaze/snappymail/issues/1204)
- Convert getPdoAccessData() : array to a RainLoop\Pdo\Settings object instance
- New bidi buttons to Squire editor by @rezaei92
[#1200](https://github.com/the-djmaze/snappymail/pull/1200)
- New bidi buttons to Squire editor
[#1200](https://github.com/the-djmaze/snappymail/pull/1200) by @rezaei92
### Fixed
- Undefined constant PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT
[#1205](https://github.com/the-djmaze/snappymail/issues/1205)
- 'reloadTime' function result is passed into 'setInterval' instead of the function by @SergeyMosin
[#1204](https://github.com/the-djmaze/snappymail/pull/1204)
- 'reloadTime' function result is passed into 'setInterval' instead of the function
[#1204](https://github.com/the-djmaze/snappymail/pull/1204) by @SergeyMosin
- UNKNOWN-CTE Invalid data in MIME part
[#1186](https://github.com/the-djmaze/snappymail/issues/1186)
@ -591,10 +975,10 @@
### Changed
- Optical issue with input fields for mail and folder search
[#1149](https://github.com/the-djmaze/snappymail/issues/1149)
- Update Chinese translation by @mayswind
[#1157](https://github.com/the-djmaze/snappymail/pull/1157)
- Update Polish translation by @tinola
[#1156](https://github.com/the-djmaze/snappymail/pull/1156)
- Update Chinese translation
[#1157](https://github.com/the-djmaze/snappymail/pull/1157) by @mayswind
- Update Polish translation
[#1156](https://github.com/the-djmaze/snappymail/pull/1156) by @tinola
### Fixed
- Undefined SIG constants
@ -623,18 +1007,18 @@
- Allow empty message body when there are attachments
[#1052](https://github.com/the-djmaze/snappymail/issues/1052)
- PHP inherit logger as Trait
- Update Portuguese by @ner00
[#1124](https://github.com/the-djmaze/snappymail/pull/1124)
- Update Traditional Chinese (Taiwan) by @chiyi4488
[#1107](https://github.com/the-djmaze/snappymail/pull/1107)
- Update Russian by @konkere
[#1108](https://github.com/the-djmaze/snappymail/pull/1108)
- Update Italian by @cm-schl
[#1094](https://github.com/the-djmaze/snappymail/pull/1094)
- Update French by @hguilbert
[#1102](https://github.com/the-djmaze/snappymail/pull/1102)
- Update German by @cm-schl
[#1087](https://github.com/the-djmaze/snappymail/pull/1087)
- Update Portuguese
[#1124](https://github.com/the-djmaze/snappymail/pull/1124) by @ner00
- Update Traditional Chinese (Taiwan)
[#1107](https://github.com/the-djmaze/snappymail/pull/1107) by @chiyi4488
- Update Russian
[#1108](https://github.com/the-djmaze/snappymail/pull/1108) by @konkere
- Update Italian
[#1094](https://github.com/the-djmaze/snappymail/pull/1094) by @cm-schl
- Update French
[#1102](https://github.com/the-djmaze/snappymail/pull/1102) by @hguilbert
- Update German
[#1087](https://github.com/the-djmaze/snappymail/pull/1087) by @cm-schl
### Fixed
- Show messagelist timeout/abort error for
@ -695,8 +1079,8 @@
- Workaround Upgrade with Nextcloud stuck in Maintenance mode
[#1046](https://github.com/the-djmaze/snappymail/issues/1046)
- Use Actions()->decodeRawKey()
- French language updated by @hguilbert
[#1045](https://github.com/the-djmaze/snappymail/pull/1045)
- French language updated
[#1045](https://github.com/the-djmaze/snappymail/pull/1045) by @hguilbert
- Sorting in folders not working
[#1022](https://github.com/the-djmaze/snappymail/issues/1022)
- Allow saving draft with empty body
@ -760,15 +1144,15 @@
### Changed
- Improved FolderCollection handling
- MODSEQ requires 64-bit int
- Update russian language by @Akrobs
[#994](https://github.com/the-djmaze/snappymail/pull/994)
- Update russian language
[#994](https://github.com/the-djmaze/snappymail/pull/994) by @Akrobs
- Don't make font bigger when screen > 1400px
- Put top menu "accountPlace" inside top-system-dropdown-id
- Put attachment controls inside attachmentsPlace
- Show message toolbar on screens > 1400px
[#970](https://github.com/the-djmaze/snappymail/issues/970)
- Chinese updated by @mayswind
[#1011](https://github.com/the-djmaze/snappymail/pull/1011)
- Chinese updated
[#1011](https://github.com/the-djmaze/snappymail/pull/1011) by @mayswind
- Prevent folder/messages flags conflict by using the right name `attributes` for Folders
- FolderInformation() use jsonSerialize()
- Squire space handling on paste use `\u00A0` instead of `&nbsp;` for
@ -811,16 +1195,16 @@
### Changed
- Reduce/simplify CSS footprint
- Use the System/Browser font by default by @HeySora
[#988](https://github.com/the-djmaze/snappymail/pull/988)
- Use the System/Browser font by default
[#988](https://github.com/the-djmaze/snappymail/pull/988) by @HeySora
- Make layout fully responsive using matchMedia('(max-width: 799px)')
- Move brotli and gzip compress option to application.ini
- After page refreshes in background whilst editing a draft, the space bar stops working
[#860](https://github.com/the-djmaze/snappymail/issues/860)
- Updated Portuguese by @ner00
[#984](https://github.com/the-djmaze/snappymail/pull/984)
- Updated French by @hguilbert
[#985](https://github.com/the-djmaze/snappymail/pull/985)
- Updated Portuguese
[#984](https://github.com/the-djmaze/snappymail/pull/984) by @ner00
- Updated French
[#985](https://github.com/the-djmaze/snappymail/pull/985) by @hguilbert
### Fixed
- unset border-box for message body
@ -916,8 +1300,8 @@
- Workaround "server connection error"
[#936](https://github.com/the-djmaze/snappymail/issues/936)
- Cleanup AbstractViewPopup handling
- Italian translation by @lota
[#948](https://github.com/the-djmaze/snappymail/pull/948)
- Italian translation
[#948](https://github.com/the-djmaze/snappymail/pull/948) by @lota
- Replace Element.fromHTML() with createElement()
### Fixed
@ -965,8 +1349,8 @@
## 2.25.5 – 2023-02-09
### Added
- New dark themes by @TheCuteFoxxy
[#925](https://github.com/the-djmaze/snappymail/pull/925)
- New dark themes
[#925](https://github.com/the-djmaze/snappymail/pull/925) by @TheCuteFoxxy
- External images option 'Always when DKIM is valid' for
[#201](https://github.com/the-djmaze/snappymail/issues/201)
- Image whitelist menu on message for
@ -1006,8 +1390,8 @@
### Added
- message maximum quoted text level for speed.
[#902](https://github.com/the-djmaze/snappymail/issues/902)
- LoveDark Theme by @TheCuteFoxxy
[#913](https://github.com/the-djmaze/snappymail/pull/913)
- LoveDark Theme
[#913](https://github.com/the-djmaze/snappymail/pull/913) by @TheCuteFoxxy
### Changed
- Moved pluginEnable() to \SnappyMail\Repository::enablePackage()
@ -1089,8 +1473,8 @@
[#188](https://github.com/the-djmaze/snappymail/issues/188)
- /static/.htaccess as mentioned by @dbiczo in
[#895](https://github.com/the-djmaze/snappymail/issues/895)
- Update nb-NO language by @master3395
[#896](https://github.com/the-djmaze/snappymail/pull/896)
- Update nb-NO language
[#896](https://github.com/the-djmaze/snappymail/pull/896) by @master3395
- \RainLoop\Utils::SetCookie() to \SnappyMail\Cookies::set()
- Merge \MailSo\Base\Utils::RecRmDir() and \MailSo\Base\Utils::RecTimeDirRemove()
@ -1115,8 +1499,8 @@
- Support RFC 8689
- Nextcloud move themes outside of app folder
[#875](https://github.com/the-djmaze/snappymail/issues/875)
- Add check to unregister system addressbook by @akhil1508
[#879](https://github.com/the-djmaze/snappymail/pull/879)
- Add check to unregister system addressbook
[#879](https://github.com/the-djmaze/snappymail/pull/879) by @akhil1508
### Changed
- material-design checkbox use hidden input for accessibility
@ -1140,8 +1524,8 @@
## 2.25.0 – 2023-01-26
### Changed
- Right margin to folder search input wrapper by @codiflow
[#871](https://github.com/the-djmaze/snappymail/pull/871)
- Right margin to folder search input wrapper
[#871](https://github.com/the-djmaze/snappymail/pull/871) by @codiflow
- Almost all request params now use JavaScript camelCase instead of CamelCase
- #rl-content to use flex for better layout control
- Decode some Copernica link tracking
@ -1170,8 +1554,8 @@
### Added
- Preparations for RFC 8689
- Add admin setting for the contacts suggestions limit
- A button to clear folder search input field by @codiflow
[#847](https://github.com/the-djmaze/snappymail/pull/847)
- A button to clear folder search input field
[#847](https://github.com/the-djmaze/snappymail/pull/847) by @codiflow
- Preparations for custom SMTP credentials handling for
[#859](https://github.com/the-djmaze/snappymail/issues/859)
[#458](https://github.com/the-djmaze/snappymail/issues/458)
@ -1187,8 +1571,8 @@
[#849](https://github.com/the-djmaze/snappymail/issues/849)
- 64-bit PHP was required
[#852](https://github.com/the-djmaze/snappymail/issues/852)
- Update fr-FR translations by @dominiquefournier
[#854](https://github.com/the-djmaze/snappymail/pull/854)
- Update fr-FR translations
[#854](https://github.com/the-djmaze/snappymail/pull/854) by @dominiquefournier
- Move createDomain & createDomainAlias buttons below table
- Move some Nextcloud SnappyMailHelper code to new InstallStep
- Cleanup messages cache handling
@ -1214,8 +1598,8 @@
### Added
- Support for search criterias ON, SENTON, SENTSINCE and SENTBEFORE
- New 'forward as attachment' glyph
- NC integration app metadata: add links to admin and dev docs by @p-bo
[#820](https://github.com/the-djmaze/snappymail/pull/820)
- NC integration app metadata: add links to admin and dev docs
[#820](https://github.com/the-djmaze/snappymail/pull/820) by @p-bo
- Clicking on messagelist grouped date/from searches on that date/from
[#815](https://github.com/the-djmaze/snappymail/issues/815)
- Nextcloud Improved file picker layout
@ -1229,16 +1613,16 @@
- Resolve layout issues by changing from `fixed` to `relative`
[#686](https://github.com/the-djmaze/snappymail/issues/686)
- Cache handling of messagelist changed due to etag issues
- Improve german and italian translation by @cm-schl
[#846](https://github.com/the-djmaze/snappymail/pull/846)
- Improve german and italian translation
[#846](https://github.com/the-djmaze/snappymail/pull/846) by @cm-schl
### Fixed
- Searching on Nextcloud search
[#787](https://github.com/the-djmaze/snappymail/issues/787)
- Workaround another Nextcloud disallowed .htaccess
[#790](https://github.com/the-djmaze/snappymail/issues/790)
- Compact display of folders in nextcloud by @makoehr
[#824](https://github.com/the-djmaze/snappymail/pull/824)
- Compact display of folders in nextcloud
[#824](https://github.com/the-djmaze/snappymail/pull/824) by @makoehr
- Admin -> Contacts PDO test failed when using different AddressBookInterface
- KolabAddressBook errors
- Forgot debug js/css setting change in Nextcloud
@ -1290,8 +1674,8 @@
### Changed
- Disable sorting when viewing message thread
[#445](https://github.com/the-djmaze/snappymail/issues/445)
- Update Chinese translation by @mayswind
[#794](https://github.com/the-djmaze/snappymail/pull/794)
- Update Chinese translation
[#794](https://github.com/the-djmaze/snappymail/pull/794) by @mayswind
- No need to call IMAP EXAMINE when current folder already SELECT
- Thread view now has tree indentation
@ -1404,8 +1788,8 @@
### Fixed
- Loosing HTML signature in account identity under settings
[#750](https://github.com/the-djmaze/snappymail/issues/750)
- Plugin configuration did not load anymore when type was SELECTION by @cm-schl
[#753](https://github.com/the-djmaze/snappymail/pull/753)
- Plugin configuration did not load anymore when type was SELECTION
[#753](https://github.com/the-djmaze/snappymail/pull/753) by @cm-schl
- Nextcloud Default theme shows gray text on gray background
[#754](https://github.com/the-djmaze/snappymail/issues/754)
- Only run JSON hooks when $sAction is set
@ -1669,8 +2053,8 @@
[#638](https://github.com/the-djmaze/snappymail/issues/638)
### Fixed
- ldap-identities-plugin by @cm-schl
[#647](https://github.com/the-djmaze/snappymail/pull/647)
- ldap-identities-plugin
[#647](https://github.com/the-djmaze/snappymail/pull/647) by @cm-schl
- OpenSSL v3 ciphers issue
[#641](https://github.com/the-djmaze/snappymail/issues/641)
@ -1769,8 +2153,8 @@
[#620](https://github.com/the-djmaze/snappymail/issues/620)
- PutinICS does is not working for all calendar events
[#622](https://github.com/the-djmaze/snappymail/issues/622)
- Update readme by @cm-schl
[#617](https://github.com/the-djmaze/snappymail/pull/617)
- Update readme
[#617](https://github.com/the-djmaze/snappymail/pull/617) by @cm-schl
## 2.20.0 – 2022-11-03
@ -1864,8 +2248,8 @@
[#638](https://github.com/the-djmaze/snappymail/issues/638)
### Fixed
- ldap-identities-plugin by @cm-schl
[#647](https://github.com/the-djmaze/snappymail/pull/647)
- ldap-identities-plugin
[#647](https://github.com/the-djmaze/snappymail/pull/647) by @cm-schl
- OpenSSL v3 ciphers issue
[#641](https://github.com/the-djmaze/snappymail/issues/641)
@ -1964,8 +2348,8 @@
[#620](https://github.com/the-djmaze/snappymail/issues/620)
- PutinICS does is not working for all calendar events
[#622](https://github.com/the-djmaze/snappymail/issues/622)
- Update readme by @cm-schl
[#617](https://github.com/the-djmaze/snappymail/pull/617)
- Update readme
[#617](https://github.com/the-djmaze/snappymail/pull/617) by @cm-schl
## 2.20.0 – 2022-11-03

View file

@ -49,7 +49,7 @@ This fork of RainLoop has the following changes:
* Admin uses password_hash/password_verify
* Auth failed attempts written to syslog
* Added Fail2ban instructions
* ES2018
* ES2020
* PHP 7.4+ required
* PHP mbstring extension required
* PHP replaced pclZip with PharData and ZipArchive
@ -66,7 +66,7 @@ This fork of RainLoop has the following changes:
* CRLF => LF line endings
* Embed boot.js and boot.css into index.html
* Removal of old JavaScript code (things are native these days)
* Added modified [Squire](https://github.com/neilj/Squire) HTML editor as replacement for CKEditor
* Added modified [Squire](https://github.com/the-djmaze/Squire/tree/snappymail) HTML editor as replacement for CKEditor
* Updated [Sabre/VObject](https://github.com/sabre-io/vobject)
* Split Admin specific JavaScript code from User code
* Split Sieve specific JavaScript code from User code
@ -140,28 +140,28 @@ RainLoop 1.17 vs SnappyMail
|js/* |RainLoop |Snappy |
|--------------- |--------: |--------: |
|admin.js |2.170.153 | 82.845 |
|app.js |4.207.787 | 430.201 |
|boot.js | 868.735 | 4.147 |
|libs.js | 658.812 | 201.911 |
|sieve.js | 0 | 84.703 |
|admin.js |2.170.153 | 84.925 |
|app.js |4.207.787 | 447.263 |
|boot.js | 868.735 | 4.343 |
|libs.js | 658.812 | 233.728 |
|sieve.js | 0 | 91.418 |
|polyfills.js | 334.608 | 0 |
|serviceworker.js | 0 | 285 |
|TOTAL |8.240.095 | 804.092 |
|TOTAL |8.240.095 | 861.962 |
|js/min/* |RainLoop |Snappy |RL gzip |SM gzip |RL brotli |SM brotli |
|--------------- |--------: |--------: |------: |------: |--------: |--------: |
|admin.min.js | 256.831 | 40.733 | 73.606 | 13.649 | 60.877 | 12.228 |
|app.min.js | 515.367 | 195.260 |139.456 | 66.275 |110.485 | 56.682 |
|boot.min.js | 84.659 | 2.087 | 26.998 | 1.204 | 23.643 | 1.002 |
|libs.min.js | 584.772 | 92.482 |180.901 | 34.729 |155.182 | 31.017 |
|sieve.min.js | 0 | 41.164 | 0 | 10.365 | 0 | 9.359 |
|admin.min.js | 256.831 | 41.719 | 73.606 | 14.022 | 60.877 | 12.567 |
|app.min.js | 515.367 | 202.101 |139.456 | 68.505 |110.485 | 58.481 |
|boot.min.js | 84.659 | 2.231 | 26.998 | 1.271 | 23.643 | 1.067 |
|libs.min.js | 584.772 | 110.646 |180.901 | 39.518 |155.182 | 35.207 |
|sieve.min.js | 0 | 45.504 | 0 | 11.131 | 0 | 9.917 |
|polyfills.min.js | 32.837 | 0 | 11.406 | 0 | 10.175 | 0 |
|TOTAL user |1.217.635 | 289.829 |358.761 |102.208 |299.485 | 88.701 |
|TOTAL user+sieve |1.217.635 | 330.993 |358.761 |112.573 |299.485 | 98.060 |
|TOTAL admin | 959.099 | 135.302 |292.911 | 49.582 |249.877 | 44.247 |
|TOTAL user |1.217.635 | 314.978 |358.761 |109.294 |299.485 | 94.755 |
|TOTAL user+sieve |1.217.635 | 360.482 |358.761 |120.425 |299.485 |104.672 |
|TOTAL admin | 959.099 | 154.596 |292.911 | 54.811 |249.877 | 48.841 |
For a user it is around 69% smaller and faster than traditional RainLoop.
For a user it is around 66% smaller and faster than traditional RainLoop.
### CSS changes
@ -188,12 +188,12 @@ For a user it is around 69% smaller and faster than traditional RainLoop.
|css/* |RainLoop |Snappy |RL gzip |SM gzip |SM brotli |
|------------ |-------: |------: |------: |------: |--------: |
|app.css | 340.331 | 84.531 | 46.946 | 17.627 | 15.111 |
|app.min.css | 274.947 | 67.906 | 39.647 | 15.544 | 13.567 |
|app.css | 340.331 | 85.073 | 46.946 | 17.792 | 15.210 |
|app.min.css | 274.947 | 68.272 | 39.647 | 15.615 | 13.636 |
|boot.css | | 1.326 | | 664 | 545 |
|boot.min.css | | 1.071 | | 590 | 474 |
|admin.css | | 30.573 | | 7.011 | 6.101 |
|admin.min.css | | 24.689 | | 6.334 | 5.575 |
|admin.css | | 30.880 | | 7.045 | 6.127 |
|admin.min.css | | 24.959 | | 6.368 | 5.615 |
### PGP
RainLoop uses the old OpenPGP.js v2
@ -207,7 +207,7 @@ See https://github.com/the-djmaze/openpgpjs for development
|OpenPGP |RainLoop |Snappy |RL gzip |SM gzip |RL brotli |SM brotli |
|--------------- |--------: |--------: |------: |-------: |--------: |--------: |
|openpgp.min.js | 330.742 | 546.309 |102.388 | 169.249 | 84.241 | 138.751 |
|openpgp.min.js | 330.742 | 546.165 |102.388 | 169.207 | 84.241 | 138.688 |
|openpgp.worker | 1.499 | | 824 | | 695 | |

View file

@ -6,8 +6,8 @@ Currently due to the fast development only the latest version receives security
| Version | Supported |
| -------- | --------- |
| 2.30.x | ✔ |
| < 2.30.0 | ❌ |
| 2.34.x | ✔ |
| < 2.34.0 | ❌ |
## Reporting a Vulnerability
@ -36,5 +36,5 @@ YMXQVq4SJt7UpWABAORudfJxsBqCRKtPlZMgGTJLjcOkyFJ9C2Fx7DeN0J4I
## Publishing and Credits
I will analyze and fix the reported issue as fast as possible.
Together with the reporter I plan the disclosure of the found and fixed vulnerability.
Together with the reporter, I plan the disclosure of the found and fixed vulnerability.
Credits to the reporter are granted and can be included in all public communication if desired.

View file

@ -9,7 +9,7 @@ if (isset($options['plugins'])) {
require(ROOT_DIR . '/build/plugins.php');
}
$gulp = trim(`which gulp`);
$gulp = trim(shell_exec('which gulp'));
if (!$gulp) {
exit('gulp not installed, run as root: npm install --global gulp-cli');
}
@ -34,7 +34,7 @@ $options['aur'] = isset($options['aur']);
// Docker build
$options['docker'] = isset($options['docker']);
if ($options['docker'] && !($docker = trim(`which docker`))) {
if ($options['docker'] && !($docker = trim(shell_exec('which docker')))) {
exit('docker not found');
}
if ($options['docker'] && $options['aur']) {
@ -60,7 +60,7 @@ if (!isset($options['skip-gulp'])) {
$cmddir = escapeshellcmd(ROOT_DIR) . '/snappymail/v/0.0.0/static';
if ($gzip = trim(`which gzip`)) {
if ($gzip = trim(shell_exec('which gzip'))) {
echo "\x1b[33;1m === Gzip *.js and *.css === \x1b[0m\n";
passthru("{$gzip} -k --best {$cmddir}/js/*.js");
passthru("{$gzip} -k --best {$cmddir}/js/min/*.js");
@ -70,7 +70,7 @@ if (!isset($options['skip-gulp'])) {
unlink(ROOT_DIR . '/snappymail/v/0.0.0/static/js/min/boot.min.js.gz');
}
if ($brotli = trim(`which brotli`)) {
if ($brotli = trim(shell_exec('which brotli'))) {
echo "\x1b[33;1m === Brotli *.js and *.css === \x1b[0m\n";
passthru("{$brotli} -k --best {$cmddir}/js/*.js");
passthru("{$brotli} -k --best {$cmddir}/js/min/*.js");
@ -188,7 +188,7 @@ file_put_contents("{$destPath}core.json", '{
// Arch User Repository
if ($options['aur']) {
// extension_loaded('blake2')
if (!function_exists('b2sum') && $b2sum = trim(`which b2sum`)) {
if (!function_exists('b2sum') && $b2sum = trim(shell_exec('which b2sum'))) {
function b2sum($file) {
$file = escapeshellarg($file);
exec("b2sum --binary {$file} 2>&1", $output, $exitcode);

View file

@ -1,7 +1,7 @@
import 'External/User/ko';
import { SMAudio } from 'Common/Audio';
import { pInt } from 'Common/Utils';
import { arrayLength } from 'Common/Utils';
import { mailToHelper, setLayoutResizer, dropdownsDetectVisibility, loadAccountsAndIdentities } from 'Common/UtilsUser';
import {
@ -27,7 +27,7 @@ import {
getFolderFromCacheList
} from 'Common/Cache';
import { i18n, reloadTime } from 'Common/Translator';
import { i18n, reloadTime, getErrorMessage } from 'Common/Translator';
import { SettingsUserStore } from 'Stores/User/Settings';
import { NotificationUserStore } from 'Stores/User/Notification';
@ -88,6 +88,8 @@ export class AppUser extends AbstractApp {
this.messageList = MessagelistUserStore;
this.ask = AskPopupView;
this.loadAccountsAndIdentities = loadAccountsAndIdentities;
}
/**
@ -112,7 +114,7 @@ export class AppUser extends AbstractApp {
case FolderType.Trash:
oMoveFolder = getFolderFromCacheList(FolderUserStore.trashFolder());
nSetSystemFoldersNotification = iFolderType;
bDelete = bDelete || UNUSED_OPTION_VALUE === FolderUserStore.trashFolder()
bDelete = bDelete/* || UNUSED_OPTION_VALUE === FolderUserStore.trashFolder()*/
|| sFromFolderFullName === FolderUserStore.spamFolder()
|| sFromFolderFullName === FolderUserStore.trashFolder();
break;
@ -147,8 +149,10 @@ export class AppUser extends AbstractApp {
}
logout() {
localStorage.removeItem('register_protocol_offered');
Remote.request('Logout', () => rl.logoutReload(Settings.app('customLogoutLink')));
Remote.request('Logout', (iError, data) =>
iError ? alert('Logout error: ' + getErrorMessage(iError, data))
: rl.logoutReload(Settings.app('customLogoutLink'))
);
}
bootstart() {
@ -180,15 +184,15 @@ export class AppUser extends AbstractApp {
SettingsUserStore.init();
ContactUserStore.init();
loadFolders(value => {
loadFolders((success, error) => {
try {
if (value) {
if (success) {
startScreens([
MailBoxUserScreen,
SettingsUserScreen
]);
setRefreshFoldersInterval(pInt(SettingsGet('CheckMailInterval')));
setRefreshFoldersInterval(SettingsGet('CheckMailInterval'));
loadAccountsAndIdentities();
@ -222,19 +226,9 @@ export class AppUser extends AbstractApp {
SMimeUserStore.loadCertificates();
setTimeout(() => mailToHelper(SettingsGet('mailToEmail')), 500);
if (!localStorage.getItem('register_protocol_offered')) {
// When auto-login is active
navigator.registerProtocolHandler?.(
'mailto',
location.protocol + '//' + location.host + location.pathname + '?mailto&to=%s',
(SettingsGet('title') || 'SnappyMail')
);
localStorage.setItem('register_protocol_offered', '1');
}
} else {
this.logout();
alert('Folders error: ' + getErrorMessage(0, error))
}
} catch (e) {
console.error(e);
@ -247,18 +241,31 @@ export class AppUser extends AbstractApp {
showMessageComposer(params = [])
{
showScreenPopup(ComposePopupView, params);
let msg = params[1];
if (1 == arrayLength(msg)) {
msg = msg[0];
}
if (msg) {
msg.decrypt().then((/*success*/)=>showScreenPopup(ComposePopupView, params));
} else {
showScreenPopup(ComposePopupView, params);
}
}
}
AskPopupView.password = function(sAskDesc, btnText) {
AskPopupView.password = function(sAskDesc, btnText, ask) {
return new Promise(resolve => {
this.showModal([
sAskDesc,
view => resolve({password:view.passphrase(), remember:view.remember()}),
view => resolve({
password:view.passphrase(),
username:/*ask & 2 ? */view.username(),
remember:/*ask & 4 ? */view.remember(),
rememberPermanent: view.rememberPermanent(),
}),
() => resolve(null),
true,
5,
ask || 1,
btnText
]);
});

View file

@ -71,7 +71,9 @@ MessageSetAction = {
SetSeen: 0,
UnsetSeen: 1,
SetFlag: 2,
UnsetFlag: 3
UnsetFlag: 3,
SetDeleted: 4,
UnsetDeleted: 5
},
/**

View file

@ -1,7 +1,7 @@
/* eslint key-spacing: 0 */
/* eslint quote-props: 0 */
import { arrayLength } from 'Common/Utils';
import { arrayLength, pInt } from 'Common/Utils';
export const RFC822 = 'message/rfc822';
@ -274,8 +274,7 @@ export const FileInfo = {
},
friendlySize: bytes => {
bytes = parseInt(bytes, 10);
bytes = isFinite(bytes) ? bytes : 0;
bytes = pInt(bytes);
let i = bytes ? Math.floor(Math.log(bytes) / Math.log(1024)) : 0;
return (bytes / Math.pow(1024, i)).toFixed(2>i ? 0 : 1) + ' ' + sizes[i];
}

View file

@ -1,6 +1,8 @@
import { RFC822 } from 'Common/File';
import { getFolderInboxName, getFolderFromCacheList } from 'Common/Cache';
import { isArray, arrayLength } from 'Common/Utils';
import { baseCollator } from 'Common/Translator';
import { SettingsGet } from 'Common/Globals';
import { isArray, arrayLength, pInt } from 'Common/Utils';
import { SettingsUserStore } from 'Stores/User/Settings';
import { FolderUserStore } from 'Stores/User/Folder';
import { MessagelistUserStore } from 'Stores/User/Messagelist';
@ -8,13 +10,13 @@ import { MessagelistUserStore } from 'Stores/User/Messagelist';
import Remote from 'Remote/User/Fetch';
let refreshInterval,
// Default every 5 minutes
refreshFoldersInterval = 300000;
// Default every 15 minutes
refreshFoldersInterval = 900000;
export const
setRefreshFoldersInterval = minutes => {
refreshFoldersInterval = Math.max(5, minutes) * 60000;
refreshFoldersInterval = Math.max(1, pInt(SettingsGet('minRefreshInterval')), pInt(minutes)) * 60000;
clearInterval(refreshInterval);
refreshInterval = setInterval(() => {
const cF = FolderUserStore.currentFolderFullName(),
@ -27,7 +29,7 @@ setRefreshFoldersInterval = minutes => {
sortFolders = folders => {
try {
let collator = new Intl.Collator(undefined, {numeric: true, sensitivity: 'base'});
let collator = baseCollator(true);
folders.sort((a, b) =>
a.isInbox() ? -1 : (b.isInbox() ? 1 : collator.compare(a.fullName, b.fullName))
);
@ -197,7 +199,7 @@ dropFilesInFolder = (sFolderFullName, files) => {
data.append('folder', sFolderFullName);
data.append('appendFile', file);
Remote.request('FolderAppend', (iError, data)=>{
iError && console.error(data.ErrorMessage);
iError && console.error(data.message);
0 == --count
&& FolderUserStore.currentFolderFullName() == sFolderFullName
&& MessagelistUserStore.reload(true, true);

View file

@ -4,6 +4,9 @@ import { SettingsUserStore } from 'Stores/User/Settings';
const
tmpl = createElement('template'),
turndown = new TurndownService(),
htmlre = /[&<>"']/g,
httpre = /^(https?:)?\/\//i,
htmlmap = {
@ -14,14 +17,34 @@ const
"'": '&#x27;'
},
disallowedTags = [
'svg','script','title','link','base','meta',
'input','output','select','button','textarea',
'bgsound','keygen','source','object','embed','applet','iframe','frame','frameset','video','audio','area','map'
// not supported by <template> element
// ,'html','head','body'
keepTagContent = 'form,button,data', // font
allowedTags = [
// Structural Elements:
'blockquote','br','div','figcaption','figure','h1','h2','h3','h4','h5','h6','hgroup','hr','p','wbr',
'article','aside','header','footer','main','section',
'details','summary','nav',
// List Elements
'dd','dl','dt','li','ol','ul',
// Text Formatting Elements
'a','abbr','address','b','bdi','bdo','cite','code','del','dfn',
'em','i','ins','kbd','mark','pre','q','rp','rt','ruby','s','samp','small',
'span','strong','sub','sup','time','u','var',
// Deprecated by HTML Standard
'acronym','big','center','dir','font','marquee',
'nobr','plaintext','rb','rtc','strike','tt',
// Media Elements
'img',//'picture','source',
// Table Elements
'caption','col','colgroup','table','tbody','td','tfoot','th','thead','tr',
// Disallowed but converted later
'style','xmp'
].join(','),
nonEmptyTags = [
'A','B','EM','I','SPAN','STRONG'
],
blockquoteSwitcher = () => {
SettingsUserStore.collapseBlockquotes() &&
// tmpl.content.querySelectorAll('blockquote').forEach(node => {
@ -99,8 +122,10 @@ const
},
cleanCSS = source =>
source.trim().replace(/(^|;)\s*-(ms|webkit)-[^;]+(;|$)/g, '')
.replace(/white-space[^;]+(;|$)/g, '')
source.trim()
.replace(/;\s*-[^;]+/g, '')
.replace(/^\s*-[^;]+(;|$)/g, '')
.replace(/white-space[^;]+/g, '')
// Drop Microsoft Office style properties
// .replace(/mso-[^:;]+:[^;]+/gi, '')
,
@ -142,14 +167,15 @@ const
if (source) {
source = source
// strip comments
.replace(/\/\*[\s\S]*?\*\/|<!--|-->/gi, '')
// strip import statements
.replace(/@import .*?;/gi , '')
// strip keyframe statements
.replace(/((@.*?keyframes [\s\S]*?){([\s\S]*?}\s*?)})/gi, '');
.replace(/\/\*[\s\S]*?\*\//gi, '')
// strip MS Word comments
.replace(/<!--[\s\S]*?-->/gi, '')
// strip HTML, as < is no CSS combinator anyway
.replace(/<[\s\S]*/gi, '');
// .replace(/<\/?[a-z][\s\S]*?>/gi, '');
// unified regex to match css & media queries together
let unified = /((\s*?(?:\/\*[\s\S]*?\*\/)?\s*?@media[\s\S]*?){([\s\S]*?)}\s*?})|(([\s\S]*?){([\s\S]*?)})/gi,
let unified = /(?:(\s*?@(?:media)[\s\S]*?){([\s\S]*?)}\s*?})|(?:([\s\S]*?){([\s\S]*?)})/gi,
arr;
while (true) {
@ -158,7 +184,7 @@ const
break;
}
let selector = arr[arr[2] === undefined ? 5 : 2].split('\r\n').join('\n').trim()
let selector = arr[arr[2] === undefined ? 3 : 1].split('\r\n').join('\n').trim()
// Never have more than a single line break in a row
.replace(/\n+/, "\n")
// Remove :root and html
@ -170,13 +196,14 @@ const
css.push({
selector: selector,
type: 'media',
subStyles: parseCSS(arr[3] + '\n}') //recursively parse media query inner css
subStyles: parseCSS(arr[2] + '\n}') //recursively parse media query inner css
});
} else if (selector && !selector.includes('@')) {
// we have standard css
// ignores @import, @keyframe, @font-face statements
css.push({
selector: selector,
rules: cleanCSS(arr[6])
rules: cleanCSS(arr[4])
});
}
}
@ -205,6 +232,7 @@ export const
detectHiddenImages = true, // !!SettingsGet('try_to_detect_hidden_images'),
bqLevel = parseInt(SettingsUserStore.maxBlockquotesLevel()),
minFontSize = parseInt(SettingsUserStore.minFontSize()),
result = {
hasExternals: false,
@ -212,6 +240,8 @@ export const
linkedData: []
},
isMsg = !!msgId,
findAttachmentByCid = cId => oAttachments.findByCid(cId),
findLocationByCid = cId => {
const attachment = findAttachmentByCid(cId);
@ -228,6 +258,15 @@ export const
rightmargin: (value, node) => node.style.marginRight = pInt(value) + 'px'
},
allowedAttributes = [
// SnappyMail
// 'data-x-href-broken',
// 'data-x-href-tracking',
'data-x-src',
// 'data-x-src-hidden',
// 'data-x-src-tracking',
// 'data-x-src-broken',
// 'data-x-style-url',
// 'data-x-style-broken-urls',
// defaults
'name',
'dir', 'lang', 'style', 'title',
@ -255,9 +294,8 @@ export const
'abbr', 'scope',
// td
'colspan', 'rowspan', 'headers'
],
nonEmptyTags = [
'A','B','EM','I','SPAN','STRONG'
// others
//'class', 'id', 'target'
];
if (SettingsUserStore.allowStyles()) {
@ -271,6 +309,8 @@ export const
// Not supported by <template> element
// .replace(/<!doctype[^>]*>/gi, '')
// .replace(/<\?xml[^>]*\?>/gi, '')
// https://github.com/the-djmaze/snappymail/issues/1860
.replace(/<(\/?)x-html(\s[^>]*)?>/gi, '')
.replace(/<(\/?)head(\s[^>]*)?>/gi, '')
.replace(/<(\/?)body(\s[^>]*)?>/gi, '<$1div class="mail-body"$2>')
// .replace(/<\/?(html|head)[^>]*>/gi, '')
@ -304,13 +344,33 @@ export const
}
});
// https://github.com/the-djmaze/snappymail/issues/1125
tmpl.content.querySelectorAll(keepTagContent).forEach(oElement => replaceWithChildren(oElement));
tmpl.content.querySelectorAll(
disallowedTags
':not('+allowedTags+'),a:empty,span:empty'
+ (0 < bqLevel ? ',' + (new Array(1 + bqLevel).fill('blockquote').join(' ')) : '')
).forEach(oElement => oElement.remove());
/* // Is this slower or faster?
).forEach(oElement => {
if (!node || !node.contains(oElement)) {
oElement.remove();
node = oElement;
}
});
*/
// https://github.com/the-djmaze/snappymail/issues/1125
tmpl.content.querySelectorAll('form,button').forEach(oElement => replaceWithChildren(oElement));
// https://github.com/the-djmaze/snappymail/issues/1641
let body = tmpl.content.querySelector('.mail-body');
[...tmpl.content.querySelectorAll('.mail-body + .mail-body')]
.forEach(oElement => body.append(...oElement.childNodes));
/*
.forEach(oElement => {
let bq = createElement('blockquote');
bq.append(...oElement.childNodes);
body.replaceWith(bq);
});
*/
[...tmpl.content.querySelectorAll('*')].forEach(oElement => {
const name = oElement.tagName,
@ -331,6 +391,13 @@ export const
return;
}
if ('XMP' === name) {
const pre = createElement('pre');
pre.innerHTML = encodeHtml(oElement.innerHTML);
oElement.replaceWith(pre);
return;
}
// \MailSo\Base\HtmlUtils::ClearTags()
if ('none' == oStyle.display
|| 'hidden' == oStyle.visibility
@ -417,7 +484,7 @@ export const
}
// if (['CENTER','FORM'].includes(name)) {
if ('O:P' === name || (nonEmptyTags.includes(name) && ('' == oElement.textContent.trim()))) {
if (nonEmptyTags.includes(name) && ('' == oElement.textContent.trim())) {
('A' !== name || !oElement.querySelector('IMG')) && replaceWithChildren(oElement);
return;
}
@ -430,70 +497,73 @@ export const
*/
let skipStyle = false;
value = delAttribute('src');
if (value) {
if ('IMG' === name) {
oElement.loading = 'lazy';
let attachment;
if (value.startsWith('cid:'))
{
value = value.slice(4);
setAttribute('data-x-src-cid', value);
attachment = findAttachmentByCid(value);
if (attachment?.download) {
oElement.src = attachment.linkPreview();
oElement.title += ' ('+attachment.fileName+')';
attachment.isInline(true);
attachment.isLinked(true);
if (isMsg) {
value = isMsg && delAttribute('src');
if (value) {
if ('IMG' === name) {
oElement.loading = 'lazy';
let attachment;
if (value.startsWith('cid:'))
{
setAttribute('data-x-src', value);
value = value.slice(4);
attachment = findAttachmentByCid(value);
if (attachment?.download) {
oElement.src = attachment.linkPreview();
// oElement.title || (oElement.title = '+attachment.fileName);
attachment.isInline(true);
attachment.isLinked(true);
}
}
}
else if ((attachment = findLocationByCid(value)))
{
if (attachment.download) {
oElement.src = attachment.linkPreview();
attachment.isLinked(true);
else if ((attachment = findLocationByCid(value)))
{
if (attachment.download) {
oElement.src = attachment.linkPreview();
attachment.isLinked(true);
}
}
}
else if (detectHiddenImages
&& ((oStyle.maxHeight && 3 > pInt(oStyle.maxHeight)) // TODO: issue with 'in'
|| (oStyle.maxWidth && 3 > pInt(oStyle.maxWidth)) // TODO: issue with 'in'
|| [
'email.microsoftemail.com/open',
'github.com/notifications/beacon/',
'/track/open', // mandrillapp.com list-manage.com
'google-analytics.com'
].filter(uri => value.toLowerCase().includes(uri)).length
)) {
skipStyle = true;
oStyle.display = 'none';
// setAttribute('style', 'display:none');
setAttribute('data-x-src-hidden', value);
// result.tracking = true;
}
else if (httpre.test(value))
{
let src = stripTracking(value);
if (src != value) {
result.tracking = true;
setAttribute('data-x-src-tracking', value);
else if (detectHiddenImages
&& ((oStyle.maxHeight && 3 > pInt(oStyle.maxHeight)) // TODO: issue with 'in'
|| (oStyle.maxWidth && 3 > pInt(oStyle.maxWidth)) // TODO: issue with 'in'
|| (oStyle.width && 2 > pInt(oStyle.width))
|| [
'email.microsoftemail.com/open',
'github.com/notifications/beacon/',
'/track/open', // mandrillapp.com list-manage.com
'google-analytics.com'
].filter(uri => value.toLowerCase().includes(uri)).length
)) {
skipStyle = true;
oStyle.display = 'none';
// setAttribute('style', 'display:none');
setAttribute('data-x-src-hidden', value);
// result.tracking = true;
}
else if (httpre.test(value))
{
let src = stripTracking(value);
if (src != value) {
result.tracking = true;
setAttribute('data-x-src-tracking', value);
}
setAttribute('data-x-src', src);
result.hasExternals = true;
oElement.alt || (oElement.alt = src.replace(/^.+\/([^/?]+).*$/, '$1').slice(-20));
}
else if (value.startsWith('data:image/'))
{
oElement.src = value;
}
else
{
setAttribute('data-x-src-broken', value);
}
setAttribute('data-x-src', src);
result.hasExternals = true;
oElement.alt || (oElement.alt = src.replace(/^.+\/([^/?]+).*$/, '$1').slice(-20));
}
else if (value.startsWith('data:image/'))
{
oElement.src = value;
}
else
{
setAttribute('data-x-src-broken', value);
}
}
else
{
setAttribute('data-x-src-broken', value);
}
}
if (hasAttribute('background')) {
@ -556,12 +626,13 @@ export const
if (urls_broken.length) {
setAttribute('data-x-style-broken-urls', JSON.stringify(urls_broken));
}
/*
// https://github.com/the-djmaze/snappymail/issues/1082
if (11 > pInt(oStyle.fontSize)) {
oStyle.removeProperty('font-size');
if (minFontSize > pInt(oStyle.fontSize)) {
oStyle.fontSize = minFontSize + "px";
// oStyle.removeProperty('font-size');
}
*/
// Removes background and color
// Many e-mails incorrectly only define one, not both
// And in dark theme mode this kills the readability
@ -579,7 +650,7 @@ export const
}
});
blockquoteSwitcher();
isMsg && blockquoteSwitcher();
// return tmpl.content.firstChild;
result.html = tmpl.innerHTML.trim();
@ -591,6 +662,9 @@ export const
* @returns {string}
*/
htmlToPlain = html => {
if (SettingsUserStore.markdown()) {
return htmlToMarkdown(html);
}
const
hr = '⎯'.repeat(64),
forEach = (selector, fn) => tmpl.content.querySelectorAll(selector).forEach(fn),
@ -611,6 +685,8 @@ export const
html = html
.replace(/<pre[^>]*>([\s\S]*?)<\/pre>/gim, (...args) =>
1 < args.length ? args[1].toString().replace(/\n/g, '<br>') : '')
// Remove line duplication
.replace(/<br><\/div>/gi, '</div>')
.replace(/\r?\n/g, '')
.replace(/\s+/gm, ' ');
@ -625,6 +701,8 @@ export const
.replace(/<t[dh](\s[\s\S]*?)?>/gi, '\t')
.replace(/<\/tr(\s[\s\S]*?)?>/gi, '\n');
forEach('style', node => node.remove());
// lines
forEach('hr', node => node.replaceWith(`\n\n${hr}\n\n`));
@ -661,7 +739,9 @@ export const
forEach('a', a => {
let txt = a.textContent, href = a.href;
return a.replaceWith(
txt.trim() == href || href.includes('mailto:') ? txt : txt + ' ' + href + ' '
txt.replace(/[\s()-]+/g, '').includes(href.replace(/^[a-z]:/, '').replace(/[\s()-]+/g, ''))
? txt
: txt + ' ' + href + ' '
);
});
@ -683,6 +763,11 @@ export const
return (tmpl.content.textContent || '').trim();
},
htmlToMarkdown = html => {
tmpl.innerHTML = html;
return turndown.turndown(tmpl.content);
},
/**
* @param {string} plain
* @param {boolean} findEmailAndLinksInText = false
@ -753,6 +838,9 @@ export const
};
rl.Utils = {
cleanHtml: cleanHtml,
htmlToPlain: htmlToPlain,
plainToHtml: plainToHtml
plainToHtml: plainToHtml,
htmlToMarkdown: htmlToMarkdown
// markdownToHtml: md => marked.parse(md)
};

View file

@ -38,14 +38,14 @@ export const
/**
* @param {string} download
* @param {string=} customSpecSuffix
* @returns {string}
*/
attachmentDownload = (download, customSpecSuffix) =>
serverRequestRaw('Download', download, customSpecSuffix),
attachmentDownload = (download) =>
serverRequestRaw('Download', download),
proxy = url =>
BASE + '?/ProxyExternal/'
// + btoa(JSON.stringify([token,url]).replace(/ /g, '%20')).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''),
+ btoa(url.replace(/ /g, '%20')).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''),
// + b64EncodeJSONSafe(url.replace(/ /g, '%20')),

View file

@ -45,8 +45,7 @@ export const
}
}
if (Intl.RelativeTimeFormat) {
let rtf = new Intl.RelativeTimeFormat(doc.documentElement.lang);
return rtf.format(seconds, unit);
return (new Intl.RelativeTimeFormat(doc.documentElement.lang)).format(seconds, unit);
}
// Safari < 14
abs = Math.abs(seconds);
@ -62,7 +61,7 @@ export const
* @returns {string}
*/
i18n = (key, valueList, defaulValue) => {
let result = null == defaulValue ? key : defaulValue;
let result = defaulValue ?? key;
let path = key.split('/');
if (I18N_DATA[path[0]] && path[1]) {
result = I18N_DATA[path[0]][path[1]] || result;
@ -185,6 +184,9 @@ export const
|| '';
},
getErrorMessage = (code, data) =>
getNotification(code) || data?.messageAdditional || data?.message || data,
/**
* @param {*} code
* @returns {string}
@ -218,7 +220,6 @@ export const
}),
/**
*
* @param {string} language
* @param {boolean=} isEng = false
* @returns {string}
@ -228,6 +229,8 @@ export const
'LANGS_NAMES' + (true === isEng ? '_EN' : '') + '/' + language,
null,
language
);
),
baseCollator = numeric => new Intl.Collator(doc.documentElement.lang, {numeric: !!numeric, sensitivity: 'base'});
init();

View file

@ -10,7 +10,7 @@ export const
pInt = (value, defaultValue = 0) => {
value = parseInt(value, 10);
return isNaN(value) || !isFinite(value) ? defaultValue : value;
return isFinite(value) ? value : defaultValue;
},
defaultOptionsAfterRender = (domItem, item) =>

View file

@ -18,15 +18,21 @@ import { AccountUserStore } from 'Stores/User/Account';
import { IdentityUserStore } from 'Stores/User/Identity';
import { isArray } from 'Common/Utils';
import { showScreenPopup } from 'Knoin/Knoin';
import { IdentityPopupView } from 'View/Popup/Identity';
export const
moveAction = ko.observable(false),
// 1 = move, 2 = copy
moveAction = ko.observable(0),
dropdownsDetectVisibility = (() =>
dropdownVisibility(!!dropdowns.find(item => item.classList.contains('show')))
).debounce(50),
editIdentity = Identity => showScreenPopup(IdentityPopupView, [Identity]),
loadAccountsAndIdentities = () => {
AccountUserStore.loading(true);
IdentityUserStore.loading(true);
@ -48,6 +54,11 @@ loadAccountsAndIdentities = () => {
? items.map(identityData => IdentityModel.reviveFromJson(identityData))
: []
);
// Invoke "Update Identity" pop up right after login
// https://github.com/the-djmaze/snappymail/issues/1689
const main = IdentityUserStore.main();
main && !main.exists() && setTimeout(()=>editIdentity(main), 1000);
}
});
},
@ -121,7 +132,7 @@ computedPaginatorHelper = (koCurrentPage, koPageCount) => {
next = 0,
limit = 2;
if (1 < pageCount || (0 < pageCount && pageCount < currentPage)) {
if (1 < pageCount) {
if (pageCount < currentPage) {
fAdd(pageCount);
prev = pageCount;
@ -359,5 +370,5 @@ populateMessageBody = (oMessage, popup) => {
}
};
leftPanelDisabled.subscribe(value => value && moveAction(false));
leftPanelDisabled.subscribe(value => value && moveAction(0));
moveAction.subscribe(value => value && leftPanelDisabled(false));

View file

@ -6,7 +6,7 @@ export class CheckboxComponent {
: ko.observable(!!params.value);
this.enable = ko.isObservable(params.enable) ? params.enable
: ko.observable(undefined === params.enable || !!params.enable);
: ko.observable(params.enable ?? 1);
this.label = params.label;
}

View file

@ -124,7 +124,7 @@ export class JCard {
// VCardProperty argument
else if (arg instanceof VCardProperty) {
let propArray = this.props.get(arg.getField());
if (!(propArray === null || propArray === void 0 ? void 0 : propArray.includes(arg)))
if (!propArray?.includes(arg))
throw Error("Attempted to remove VCardProperty VCard does not have: ".concat(arg));
propArray.splice(propArray.indexOf(arg), 1);
if (propArray.length === 0)

View file

@ -3,10 +3,6 @@
(doc => {
const
removeElements = 'HEAD,LINK,META,NOSCRIPT,SCRIPT,TEMPLATE,TITLE',
allowedElements = 'A,B,BLOCKQUOTE,BR,DIV,FONT,H1,H2,H3,H4,H5,H6,HR,IMG,LI,OL,P,SPAN,STRONG,TABLE,TD,TH,TR,U,UL',
allowedAttributes = 'abbr,align,background,bgcolor,border,cellpadding,cellspacing,class,color,colspan,dir,face,frame,height,href,hspace,id,lang,rowspan,rules,scope,size,src,style,target,type,usemap,valign,vspace,width'.split(','),
i18n = (str, def) => rl.i18n(str) || def,
ctrlKey = shortcuts.getMetaKey() + ' + ',
@ -21,38 +17,17 @@ const
forEachObjectValue = (obj, fn) => Object.values(obj).forEach(fn),
getFragmentOfChildren = parent => {
let frag = doc.createDocumentFragment();
frag.append(...parent.childNodes);
return frag;
},
SquireDefaultConfig = {
/*
addLinks: true // allow_smart_html_links
*/
sanitizeToDOMFragment: (html, isPaste/*, squire*/) => {
tpl.innerHTML = (html||'')
html = (html||'')
.replace(/<\/?(BODY|HTML)[^>]*>/gi,'')
.replace(/<!--[^>]+-->/g,'')
.replace(/<span[^>]*>\s*<\/span>/gi,'')
.trim();
tpl.querySelectorAll('a:empty,span:empty').forEach(el => el.remove());
if (isPaste) {
tpl.querySelectorAll(removeElements).forEach(el => el.remove());
tpl.querySelectorAll('*').forEach(el => {
if (!el.matches(allowedElements)) {
el.replaceWith(getFragmentOfChildren(el));
} else if (el.hasAttributes()) {
[...el.attributes].forEach(attr => {
let name = attr.name.toLowerCase();
if (!allowedAttributes.includes(name)) {
el.removeAttribute(name);
}
});
}
});
}
tpl.innerHTML = isPaste ? rl.Utils.cleanHtml(html).html : html;
return tpl.content;
}
};
@ -109,13 +84,13 @@ class SquireUI
cmd: s => squire.setStyle({ fontFamily: s.value })
},
fontSize: {
select: ['11px','13px','16px','20px','24px','30px'],
defaultValueIndex: 2,
select: [[i18n('GLOBAL/DEFAULT'),''],'11px','13px','16px','20px','24px','30px'],
defaultValueIndex: 0,
cmd: s => squire.setStyle({ fontSize: s.value })
// TODO: maybe consider using https://developer.mozilla.org/en-US/docs/Web/CSS/font-size#values
// example:
// select: ['xx-small', 'x-small',' small',' medium', 'large', 'x-large', 'xx-large', 'xxx-large'],
// defaultValueIndex: 3,
// select: ['','xx-small', 'x-small',' small',' medium', 'large', 'x-large', 'xx-large', 'xxx-large'],
// defaultValueIndex: 0,
},
// dir: {
// select: [
@ -133,11 +108,11 @@ class SquireUI
dir: {
dir_ltr: {
html: '⁋',
cmd: () => squire.bidi('ltr')
cmd: () => squire.setTextDirection('ltr')
},
dir_rtl: {
html: '¶',
cmd: () => squire.bidi('rtl')
cmd: () => squire.setTextDirection('rtl')
}
},
colors: {
@ -155,7 +130,7 @@ class SquireUI
html: 'B',
cmd: () => this.doAction('bold'),
key: 'B',
matches: 'B,STRONT'
matches: 'B,STRONG'
},
italic: {
html: 'I',
@ -237,7 +212,7 @@ class SquireUI
cmd: () => {
let node = squire.getSelectionClosest('IMG'),
src = prompt("Image", node?.src || "https://");
src?.length ? squire.insertImage(src) : (node && squire.detach(node));
src?.length ? squire.insertImage(src) : node?.remove();
},
matches: 'IMG'
},
@ -355,6 +330,7 @@ class SquireUI
input.append(option);
});
} else {
input.add(new Option(i18n('GLOBAL/DEFAULT'), ''));
Object.entries(cfg.select).forEach(([label, options]) => {
let group = createElement('optgroup');
group.label = label;
@ -363,7 +339,7 @@ class SquireUI
option.style[action] = value;
group.append(option);
});
input.append(group);
input.add(group);
});
}
ev = 'input';
@ -405,9 +381,47 @@ class SquireUI
let changes = actions.changes;
changes.undo.input.disabled = changes.redo.input.disabled = true;
squire.addEventListener('undoStateChange', state => {
changes.undo.input.disabled = !state.canUndo;
changes.redo.input.disabled = !state.canRedo;
squire.addEventListener('undoStateChange', e => {
changes.undo.input.disabled = !e.detail.canUndo;
changes.redo.input.disabled = !e.detail.canRedo;
});
squire.addEventListener('pasteImage', e => {
const items = e.detail.clipboardData.items;
let l = items.length;
while (l--) {
const item = items[l];
if (/^image\/(png|jpeg|webp)/.test(item.type)) {
let reader = new FileReader();
reader.onload = event => {
let img = createElement("img"),
canvas = createElement("canvas"),
ctx = canvas.getContext('2d');
img.onload = ()=>{
ctx.drawImage(img, 0, 0);
let width = img.width, height = img.height;
if (width > height) {
// Landscape
if (width > 1024) {
height = height * 1024 / width;
width = 1024;
}
} else if (height > 1024) {
// Portrait
width = width * 1024 / height;
height = 1024;
}
canvas.width = width;
canvas.height = height;
ctx.drawImage(img, 0, 0, width, height);
squire.insertHTML('<img alt="" style="width:100%;max-width:'+width+'px" src="'+canvas.toDataURL()+'">', true);
};
img.src = event.target.result;
}
reader.readAsDataURL(item.getAsFile());
break;
}
}
});
actions.font.fontSize.input.selectedIndex = actions.font.fontSize.defaultValueIndex;
@ -477,24 +491,31 @@ class SquireUI
// -----
squire.addEventListener('pathChange', e => {
squire.addEventListener('pathChange', () => {
const squireRoot = squire.getRoot();
let range = squire.getSelection(),
collapsed = range.collapsed,
elm = collapsed ? range.endContainer : range?.commonAncestorContainer;
if (elm && !(elm instanceof Element)) {
elm = elm.parentElement;
}
forEachObjectValue(actions, entries => {
forEachObjectValue(entries, cfg => {
// cfg.matches && cfg.input.classList.toggle('active', e.element && e.element.matches(cfg.matches));
cfg.matches && cfg.input.classList.toggle('active', e.element && e.element.closestWithin(cfg.matches, squireRoot));
// Check if selection has a matching parent or contains a matching element
cfg.matches && cfg.input.classList.toggle('active', !!(elm && (
(!collapsed && [...elm.querySelectorAll(cfg.matches)].some(node => range.intersectsNode(node)))
|| elm.closestWithin(cfg.matches, squireRoot)
)));
});
});
if (e.element) {
if (elm) {
// try to find font-family and/or font-size and set "select" elements' values
let sizeSelectedIndex = actions.font.fontSize.defaultValueIndex;
let familySelectedIndex = defaultFontFamilyIndex;
let elm = e.element;
let familyFound = false;
let sizeFound = false;
do {
@ -526,10 +547,10 @@ class SquireUI
});
/*
squire.addEventListener('cursor', e => {
console.dir({cursor:e.range});
console.dir({cursor:e.detail.range});
});
squire.addEventListener('select', e => {
console.dir({select:e.range});
console.dir({select:e.detail.range});
});
*/
}

View file

@ -97,7 +97,7 @@ Object.assign(ko.bindingHandlers, {
fValue.subscribe(fUpdateEditorValue);
// ko.utils.domNodeDisposal.addDisposeCallback(element, () => {
// ko.addDisposeCallback(element, () => {
// });
}
}

7
dev/External/ko.js vendored
View file

@ -15,6 +15,7 @@ export const
* based solely on the values of other observables in the application
*/
koComputable = fn => ko.computed(fn, {'pure':true}),
// koObservable = value => ko.observable(value),
addObservablesTo = (target, observables) =>
forEachObjectEntry(observables, (key, value) =>
@ -30,7 +31,7 @@ export const
onEvent = (element, event, fn) => {
element.addEventListener(event, fn);
ko.utils.domNodeDisposal.addDisposeCallback(element, () => element.removeEventListener(event, fn));
ko.addDisposeCallback(element, () => element.removeEventListener(event, fn));
},
onKey = (key, element, fValueAccessor, fAllBindings, model) => {
@ -175,6 +176,6 @@ ko.extenders.falseTimeout = (target, option) => {
// functions
ko.observable.fn.askDeleteHelper = function() {
return this.extend({ falseTimeout: 3000, toggleSubscribeProperty: [this, 'askDelete'] });
ko.observable.fn.askDeleteHelper = function(prop='askDelete') {
return this.extend({ falseTimeout: 3000, toggleSubscribeProperty: [this, prop] });
};

View file

@ -1,55 +0,0 @@
<html>
<head>
<meta charset="utf-8"/>
<title></title>
<style>
html, body {
margin: 0;
padding: 0;
}
header {
background: rgba(125,128,128,0.3);
border-bottom: 1px solid #888;
}
header h1 {
font-size: 120%;
}
header * {
margin: 5px 0;
}
header time {
float: right;
}
blockquote {
border-left: 2px solid rgba(125,128,128,0.5);
margin: 0;
padding: 0 0 0 10px;
}
pre {
white-space: pre-wrap;
word-wrap: break-word;
word-break: normal;
}
body > * {
padding: 0.5em 1em;
}
#attachments > * {
border: 1px solid rgba(125,128,128,0.5);
padding: 0.25em;
margin-right: 1em;
}
#attachments > *::before {
content: '📎 ';
}
</style>
</head>
<body></body>
</html>

View file

@ -60,10 +60,9 @@ export class AbstractViewPopup extends AbstractView
this.keyScope.scope = name;
this.modalVisible = ko.observable(false).extend({ rateLimit: 0 });
this.close = () => this.modalVisible(false);
this.tryToClose = () => (false === this.onClose()) || this.close();
addShortcut('escape,close', '', name, () => {
if (this.modalVisible() && false !== this.onClose()) {
this.close();
}
this.modalVisible() && this.tryToClose();
return false;
// return true; Issue with supported modal close
});
@ -116,6 +115,11 @@ export class AbstractViewSettings
onHide() {}
viewModelDom
*/
/**
* When this[name] does not exists, create as observable with value of SettingsGet(name)
* When this[name+'Trigger'] does not exists, create as observable
* Subscribe to this[name], and handle saving the setting
*/
addSetting(name, valueCb)
{
let prop = name[0].toLowerCase() + name.slice(1),
@ -131,6 +135,7 @@ export class AbstractViewSettings
rl.app.Remote.saveSetting(name, value,
iError => {
this[trigger](iError ? SaveSettingStatus.Failed : SaveSettingStatus.Success);
// iError || Settings.set(name, value);
setTimeout(() => this[trigger](SaveSettingStatus.Idle), 1000);
}
);
@ -138,6 +143,10 @@ export class AbstractViewSettings
});
}
/**
* Foreach name if this[name] does not exists, create as observable with value of SettingsGet(name)
* Subscribe to this[name], for saving the setting
*/
addSettings(names)
{
names.forEach(name => {

View file

@ -74,13 +74,17 @@ const
// vmDom.addEventListener('close', () => vm.modalVisible(false));
// show/hide popup/modal
// transitionend is called for each property, so we only listen to `opacity`
// as defined in CSS by `dialog:not(.animate)`
const endShowHide = e => {
if (e.target === vmDom) {
if (e.target === vmDom && 'opacity' === e.propertyName) {
if (vmDom.classList.contains('animate')) {
vm.afterShow?.();
fireEvent('rl-vm-visible', vm);
} else {
vmDom.close();
vm.afterHide?.();
// fireEvent('rl-vm-hidden', vm);
}
}
};
@ -163,7 +167,7 @@ const
if (screenName && fireEvent('sm-show-screen', screenName + (subPart ? '/' + subPart : ''), 1)) {
// Close all popups
for (let vm of visiblePopups) {
(false === vm.onClose()) || vm.close();
vm.tryToClose();
}
let vmScreen = screen(screenName);

View file

@ -83,7 +83,7 @@ export function ParseMime(text)
get body() {
let body = this.bodyRaw,
charset = this.header('content-type')?.params.charset,
encoding = this.headerValue('content-transfer-encoding');
encoding = this.headerValue('content-transfer-encoding')?.toLowerCase();
if ('quoted-printable' == encoding) {
body = QPDecode(body);
} else if ('base64' == encoding) {
@ -94,7 +94,7 @@ export function ParseMime(text)
get dataUrl() {
let body = this.bodyRaw,
encoding = this.headerValue('content-transfer-encoding');
encoding = this.headerValue('content-transfer-encoding')?.toLowerCase();
if ('base64' == encoding) {
body = body.replace(/\r?\n/g, '');
} else {
@ -112,7 +112,7 @@ export function ParseMime(text)
}
getByContentType(type) {
if (type == this.headerValue('content-type')) {
if (type == this.headerValue('content-type')?.toLowerCase()) {
return this;
}
let i = 0, p = this.parts, part;

View file

@ -18,6 +18,7 @@ export function MimeToMessage(data, message)
subject = struct.headerValue('subject');
html = html ? html.body : '';
// Content-Type: ...; protected-headers="v1"
subject && message.subject(subject);
// EmailCollectionModel
@ -99,6 +100,4 @@ export function MimeToMessage(data, message)
if (message.plain().includes(BEGIN_PGP_MESSAGE)) {
message.pgpSigned(true);
}
// TODO: Verify instantly?
}

View file

@ -1,3 +1,4 @@
import { baseCollator } from 'Common/Translator';
import { AbstractCollectionModel } from 'Model/AbstractCollection';
import { AttachmentModel } from 'Model/Attachment';
@ -11,7 +12,7 @@ export class AttachmentCollectionModel extends AbstractCollectionModel
*/
static reviveFromJson(items) {
const attachments = super.reviveFromJson(items, attachment => AttachmentModel.reviveFromJson(attachment));
let collator = new Intl.Collator(undefined, {numeric: true, sensitivity: 'base'});
let collator = baseCollator(true);
attachments.sort((a, b) => {
if (a.isInline()) {
if (!b.isInline()) {

View file

@ -245,7 +245,7 @@ export class ContactModel extends AbstractModel {
this.namePrefix(),
this.nameSuffix()
]/*, params, group*/);
// jCard.parseFullName({set:true});
jCard.parseFullName({set:true});
['nickname', 'note', 'title'].forEach(field =>
this[field]() ? jCard.set(field, this[field]()/*, params, group*/) : jCard.remove(field)

View file

@ -73,4 +73,9 @@ export class EmailModel extends AbstractModel {
}
return result || name;
}
get domain() {
return this.email.replace(/^.+@([^@]+)$/, '$1');
// return this.email.slice(this.email.lastIndexOf('@'))
}
}

View file

@ -29,6 +29,8 @@ import { FileInfo } from 'Common/File';
import { FolderPopupView } from 'View/Popup/Folder';
import { showScreenPopup } from 'Knoin/Knoin';
import { isAllowedKeyword } from 'Stores/User/Folder';
const
// isPosNumeric = value => null != value && /^[0-9]*$/.test(value.toString()),
@ -102,7 +104,7 @@ export const
// Repeat every 15 minutes?
// this.foldersTimeout = setTimeout(loadFolders, 900000);
})
.catch(() => fCallback && setTimeout(fCallback, 1, false));
.catch(e => fCallback && setTimeout(fCallback, 1, false, e));
};
export class FolderCollectionModel extends AbstractCollectionModel
@ -411,6 +413,8 @@ export class FolderModel extends AbstractModel {
canBeSubscribed: () => this.selectable()
&& !(this.isSystemFolder() | !SettingsUserStore.hideUnsubscribed()),
optionalTags: () => this.permanentFlags.filter(isAllowedKeyword),
/**
* Folder is visible when:
* - hasVisibleSubfolders()
@ -467,16 +471,25 @@ export class FolderModel extends AbstractModel {
detailedName: () => this.name() + ' ' + this.nameInfo(),
hasSubscribedUnreadMessagesSubfolders: () =>
!!this.subFolders().find(
folder => folder.unreadEmails() | folder.hasSubscribedUnreadMessagesSubfolders()
)
/*
!!this.subFolders().filter(
folder => folder.unreadEmails() | folder.hasSubscribedUnreadMessagesSubfolders()
).length
*/
,href: () => this.canBeSelected() && mailBox(this.fullNameHash)
icon: () => {
switch (this.type())
{
case 1: return '📥'; // FolderType.Inbox
case 2: return '📧'; // FolderType.Sent icon-paper-plane
case 3: return '🗎'; // FolderType.Drafts
case 4: return '⚠'; // FolderType.Junk
case 5: return '🗑'; // FolderType.Trash
case 6: return '🗄'; // FolderType.Archive
}
return null;
},
hasUnreadInSub: () =>
this.subFolders().some(
folder => folder.unreadEmails() | folder.hasUnreadInSub()
),
href: () => this.canBeSelected() && mailBox(this.fullNameHash)
});
}

View file

@ -1,19 +1,16 @@
import { AbstractModel } from 'Knoin/AbstractModel';
//import { AbstractModel } from 'Knoin/AbstractModel';
import { EmailModel } from 'Model/Email';
import { addObservablesTo, addComputablesTo } from 'External/ko';
export class IdentityModel extends AbstractModel {
/**
* @param {string} id
* @param {string} email
*/
export class IdentityModel extends EmailModel /*AbstractModel*/ {
constructor() {
super();
addObservablesTo(this, {
id: '',
label: '',
email: '',
name: '',
// email: '',
// name: '',
replyTo: '',
bcc: '',
@ -27,23 +24,28 @@ export class IdentityModel extends AbstractModel {
smimeKey: '',
smimeCertificate: '',
smimeCertificateChain: '',
askDelete: false
askDelete: false,
exists: false
});
addComputablesTo(this, {
smimeKeyEncrypted: () => this.smimeKey().includes('-----BEGIN ENCRYPTED PRIVATE KEY-----'),
smimeKeyValid: () => /^-----BEGIN (ENCRYPTED )?PRIVATE KEY-----/.test(this.smimeKey()),
smimeCertificateValid: () => /^-----BEGIN CERTIFICATE-----/.test(this.smimeCertificate())
smimeKeyValid: () => /^-----BEGIN (ENCRYPTED |RSA )?PRIVATE KEY-----/.test(this.smimeKey()),
smimeCertificateValid: () => /^-----BEGIN CERTIFICATE-----/.test(this.smimeCertificate()),
smimeCertificateChainValid: () => !this.smimeCertificateChain()
|| /^-----BEGIN CERTIFICATE-----/.test(this.smimeCertificateChain())
});
}
/**
* @returns {string}
*/
formattedName() {
const name = this.name(),
email = this.email(),
toString() {
const name = this.name,
email = this.email,
label = this.label();
return (name ? `${name} ` : '') + `<${email}>` + (label ? ` (${label})` : '');
}

View file

@ -8,7 +8,7 @@ import { forEachObjectEntry, b64EncodeJSONSafe } from 'Common/Utils';
import { serverRequestRaw, proxy } from 'Common/Links';
import { addObservablesTo, addComputablesTo } from 'External/ko';
import { FolderUserStore, isAllowedKeyword } from 'Stores/User/Folder';
import { FolderUserStore } from 'Stores/User/Folder';
import { SettingsUserStore } from 'Stores/User/Settings';
import { FileInfo, RFC822 } from 'Common/File';
@ -18,15 +18,73 @@ import { MimeHeaderCollectionModel } from 'Model/MimeHeaderCollection';
//import { MimeHeaderAutocryptModel } from 'Model/MimeHeaderAutocrypt';
import { AbstractModel } from 'Knoin/AbstractModel';
import PreviewHTML from 'Html/PreviewMessage.html';
import { LanguageStore } from 'Stores/Language';
import Remote from 'Remote/User/Fetch';
import { MimeToMessage } from 'Mime/Utils';
import { PgpUserStore } from 'Stores/User/Pgp';
import { IdentityUserStore } from 'Stores/User/Identity';
import { Passphrases } from 'Storage/Passphrases';
const
PreviewHTML = `<html>
<head>
<meta charset="utf-8">
<title></title>
<style>
html, body {
margin: 0;
padding: 0;
}
header {
background: rgba(125,128,128,0.3);
border-bottom: 1px solid #888;
}
header h1 {
font-size: 120%;
}
header * {
margin: 5px 0;
}
header time {
float: right;
}
blockquote {
border-left: 2px solid rgba(125,128,128,0.5);
margin: 0;
padding: 0 0 0 10px;
}
pre {
white-space: pre-wrap;
word-wrap: break-word;
word-break: normal;
}
body > * {
padding: 0.5em 1em;
}
#attachments > * {
border: 1px solid rgba(125,128,128,0.5);
padding: 0.25em;
margin-right: 1em;
}
#attachments > *::before {
content: '📎 ';
}
</style>
</head>
<body></body>
</html>`,
msgHtml = msg => cleanHtml(msg.html(), msg.attachments(), '#rl-msg-' + msg.hash),
toggleTag = (message, keyword) => {
@ -81,7 +139,6 @@ export class MessageModel extends AbstractModel {
// autocrypt: ko.observableArray(),
hasVirus: null, // or boolean when scanned
priority: 3, // Normal
internalTimestamp: 0,
senderEmailsString: '',
senderClearEmailsString: '',
isSpam: false,
@ -104,8 +161,7 @@ export class MessageModel extends AbstractModel {
plain: '',
html: '',
dateTimestamp: 0,
deleted: false,
dateTimestampSource: 0,
// Also used by Selector
focused: false,
@ -152,22 +208,21 @@ export class MessageModel extends AbstractModel {
isUnseen: () => !this.flags().includes('\\seen'),
isFlagged: () => this.flags().includes('\\flagged'),
isDeleted: () => this.flags().includes('\\deleted'),
// isJunk: () => this.flags().includes('$junk') && !this.flags().includes('$nonjunk'),
// isPhishing: () => this.flags().includes('$phishing'),
tagOptions: () => {
const tagOptions = [];
FolderUserStore.currentFolder().permanentFlags.forEach(value => {
if (isAllowedKeyword(value)) {
let lower = value.toLowerCase();
tagOptions.push({
css: 'msgflag-' + lower,
value: value,
checked: this.flags().includes(lower),
label: i18n('MESSAGE_TAGS/'+lower, 0, value),
toggle: (/*obj*/) => toggleTag(this, value)
});
}
FolderUserStore.currentFolder().optionalTags().forEach(value => {
let lower = value.toLowerCase();
tagOptions.push({
css: 'msgflag-' + lower,
value: value,
checked: this.flags().includes(lower),
label: i18n('MESSAGE_TAGS/'+lower, 0, value),
toggle: (/*obj*/) => toggleTag(this, value)
});
});
return tagOptions
},
@ -311,7 +366,6 @@ export class MessageModel extends AbstractModel {
lineAsCss(flags=1) {
let classes = [];
forEachObjectEntry({
deleted: this.deleted(),
selected: this.selected(),
checked: this.checked(),
unseen: this.isUnseen(),
@ -513,7 +567,7 @@ export class MessageModel extends AbstractModel {
hasImages = true;
},
attr = 'data-x-src',
src, useProxy = !!SettingsGet('useLocalProxyForExternalImages');
src, useProxy = !!SettingsGet('proxyExternalImages');
body.querySelectorAll('img[' + attr + ']').forEach(node => {
src = node.getAttribute(attr);
if (isValid(src)) {
@ -548,4 +602,140 @@ export class MessageModel extends AbstractModel {
return result.html || plainToHtml(this.plain());
}
async decrypt() {
const msg = this;
if (msg.pgpEncrypted() && !msg.pgpDecrypted()) {
return await msg.pgpDecrypt().then(()=>msg.pgpDecrypted());
}
if (msg.smimeEncrypted() && !msg.smimeDecrypted()) {
return await msg.smimeDecrypt().then(()=>msg.smimeDecrypted());
}
return true;
}
/*
async decrypted() {
return msg.pgpDecrypted() || msg.smimeDecrypted());
}
*/
async pgpDecrypt() {
const oMessage = this,
data = oMessage.pgpEncrypted();
delete data.error;
await PgpUserStore.decrypt(oMessage).then(result => {
if (!result) {
// TODO: translate
throw Error('Decryption failed, canceled or not possible');
}
oMessage.pgpDecrypted(true);
if (result.data) {
MimeToMessage(result.data, oMessage);
oMessage.html() ? oMessage.viewHtml() : oMessage.viewPlain();
if (result.signatures?.length) {
oMessage.pgpSigned({
signatures: result.signatures,
success: !!result.signatures.length
});
}
}
})
.catch(e => {
data.error = e.message;
})
.finally(() => {
oMessage.pgpEncrypted(data);
});
}
pgpVerify(/*self, event*/) {
const oMessage = this;
PgpUserStore.verify(oMessage).then(result => {
if (result) {
oMessage.pgpSigned(result);
} else {
alert('Verification failed or no valid public key found');
}
/*
if (result?.success) {
i18n('CRYPTO/GOOD_SIGNATURE', {
USER: validKey.user + ' (' + validKey.id + ')'
});
message.getText()
} else {
const keyIds = arrayLength(signingKeyIds) ? signingKeyIds : null,
additional = keyIds
? keyIds.map(item => item?.toHex?.()).filter(v => v).join(', ')
: '';
i18n('CRYPTO/ERROR', {
TYPE: 'OpenPGP',
ERROR: 'message'
}) + (additional ? ' (' + additional + ')' : '');
}
*/
});
}
async smimeDecrypt() {
const message = this;
const addresses = message.from.concat(message.to, message.cc, message.bcc).map(item => item.email),
identity = IdentityUserStore.find(item => addresses.includes(item.email)),
data = message.smimeEncrypted(); // { partId: "1" }
if (data && identity) {
delete data.error;
let pass, params = { ...data }; // clone
params.folder = message.folder;
params.uid = message.uid;
// params.bodyPart = params.bodyPart?.raw;
params.certificate = identity.smimeCertificate();
params.privateKey = identity.smimeKey();
if (identity.smimeKeyEncrypted()) {
pass = await Passphrases.ask(identity,
i18n('SMIME/PRIVATE_KEY_OF', {EMAIL: identity.email}),
'CRYPTO/DECRYPT'
);
if (!pass) {
return;
}
params.passphrase = pass?.password;
}
await Remote.post('SMimeDecryptMessage', null, params).then(response => {
if (response?.Result?.data) {
message.smimeDecrypted(true);
MimeToMessage(response.Result.data, message);
message.html() ? message.viewHtml() : message.viewPlain();
pass && pass.remember && Passphrases.handle(identity, pass.password);
if ('signed' in response.Result) {
message.smimeSigned(response.Result.signed);
}
}
}).catch(e => {
data.error = e.message
})
.finally(() => {
message.smimeEncrypted(data);
});
}
}
smimeVerify(/*self, event*/) {
const message = this,
data = message.smimeSigned(); // { partId: "1", micAlg: "pgp-sha256" }
if (data) {
const params = { ...data }; // clone
params.folder = message.folder;
params.uid = message.uid;
params.bodyPart = data.bodyPart?.raw;
params.sigPart = data.sigPart?.bodyRaw;
Remote.post('SMimeVerifyMessage', null, params).then(response => {
if (response?.Result) {
if (response.Result.body) {
MimeToMessage(response.Result.body, message);
message.html() ? message.viewHtml() : message.viewPlain();
}
data.success = response.Result.success;
message.smimeSigned(data);
}
});
}
}
}

View file

@ -30,12 +30,13 @@ export class MessageCollectionModel extends AbstractCollectionModel
let msg = MessageUserStore.message();
return super.reviveFromJson(object, message => {
// If message is currently viewed, use that.
// Maybe then use msg.revivePropertiesFromJson(message) ?
message = (msg && msg.hash === message.hash) ? msg : MessageModel.reviveFromJson(message);
if (message) {
message.deleted(false);
return message;
if (msg && msg.hash === message.hash) {
msg.revivePropertiesFromJson(message);
message = msg;
} else {
message = MessageModel.reviveFromJson(message);
}
return message;
});
}
}

View file

@ -10,18 +10,20 @@ export class MimeHeaderAutocryptModel/* extends AbstractModel*/
if (value) {
value.split(';').forEach(entry => {
entry = entry.split('=');
entry = entry.match(/^([^=]+)=(.*)$/);
const trim = str => (str || '').trim().replace(/^["']|["']+$/g, '');
this[trim(entry[0]).replace('-', '_')] = trim(entry[1]);
this[trim(entry[1]).replace('-', '_')] = trim(entry[2]);
});
this.keydata = this.keydata.replace(/\s+/g, '\n');
}
}
toString() {
let result = `addr=${this.addr}; `;
if ('mutual' === this.prefer_encrypt) {
return `addr=${this.addr}; prefer-encrypt=mutual; keydata=${this.keydata}`;
result += 'prefer-encrypt=mutual; ';
}
return `addr=${this.addr}; keydata=${this.keydata}`;
return result + 'keydata=' + this.keydata.replace(/\n/g, '\n ');
}
pem() {

View file

@ -8,11 +8,11 @@ let iJsonErrorCount = 0;
const getURL = (add = '') => serverRequest('Json') + pString(add),
checkResponseError = data => {
const err = data ? data.ErrorCode : null;
const err = data ? data.code : null;
if (Notifications.InvalidToken === err) {
console.error(getNotification(err));
console.error(getNotification(err) + ` (${data.messageAdditional})`);
// alert(getNotification(err));
rl.logoutReload();
setTimeout(rl.logoutReload, 5000);
} else if ([
Notifications.AuthError,
Notifications.ConnectionError,
@ -131,7 +131,7 @@ export class AbstractFetchRemote
fetchJSON(sAction, getURL(sGetAdd),
sGetAdd ? null : (params || {}),
undefined === iTimeout ? 30000 : pInt(iTimeout),
pInt(iTimeout ?? 30000),
async data => {
let iError = 0;
if (data) {
@ -144,7 +144,7 @@ export class AbstractFetchRemote
iJsonErrorCount = 0;
} else {
checkResponseError(data);
iError = data.ErrorCode || Notifications.UnknownError
iError = data.code || Notifications.UnknownError
}
}
@ -196,7 +196,7 @@ export class AbstractFetchRemote
return Promise.reject(new FetchError(Notifications.JsonParse));
}
if (111 === data?.ErrorCode && rl.app.ask && await rl.app.ask.cryptkey()) {
if (111 === data?.code && rl.app.ask && await rl.app.ask.cryptkey()) {
return this.post(action, fTrigger, params, timeOut);
}
/*
@ -222,8 +222,8 @@ export class AbstractFetchRemote
if (!data.Result || action !== data.Action) {
checkResponseError(data);
return Promise.reject(new FetchError(
data ? data.ErrorCode : 0,
data ? (data.ErrorMessageAdditional || data.ErrorMessage) : ''
data ? data.code : 0,
data ? (data.messageAdditional || data.message) : ''
));
}

View file

@ -117,7 +117,7 @@ export class AbstractSettingsScreen extends AbstractScreen {
rules = {
subname: /^(.*)$/,
normalize_: (rquest, vals) => {
vals.subname = null == vals.subname ? defaultRoute : pString(vals.subname);
vals.subname = pString(vals.subname ?? defaultRoute);
return [vals.subname];
}
};

View file

@ -109,7 +109,7 @@ export class MailBoxUserScreen extends AbstractScreen {
*/
onBuild() {
doc.addEventListener('click', event =>
event.target.closest('#rl-right') && moveAction(false)
event.target.closest('#rl-right') && moveAction(0)
);
}

View file

@ -16,6 +16,7 @@ export class AdminSettingsAbout /*extends AbstractViewSettings*/ {
coreWarning: false,
coreVersion: '',
coreVersionCompare: -2,
php64: true,
load1: 0,
load5: 0,
load15: 0,
@ -69,6 +70,7 @@ export class AdminSettingsAbout /*extends AbstractViewSettings*/ {
this.coreWarning(!!data.core.warning);
this.coreVersion(data.core.version || '');
this.coreVersionCompare(data.core.versionCompare);
this.php64(data.php[1].loaded);
} else {
this.coreReal(false);
this.coreWarning(false);
@ -78,6 +80,10 @@ export class AdminSettingsAbout /*extends AbstractViewSettings*/ {
});
}
clearCache() {
Remote.request('AdminClearCache');
}
updateCoreData() {
if (!this.coreUpdating()) {
this.coreUpdating(true);

View file

@ -10,7 +10,7 @@ export class AdminSettingsConfig /*extends AbstractViewSettings*/ {
constructor() {
this.config = ko.observableArray();
this.search = ko.observableArray();
this.search = ko.observable('');
this.saved = ko.observable(false).extend({ falseTimeout: 5000 });
this.search.subscribe(value => {

View file

@ -18,8 +18,8 @@ export class AdminSettingsDomains /*extends AbstractViewSettings*/ {
testUsername() {
Remote.request('AdminDomainMatch',
(iError, oData) => {
if (oData?.Result?.domain) {
alert(`${oData.Result.email} matched domain: ${oData.Result.domain.name}`);
if (oData?.Result?.name) {
alert(`${oData.Result.email} matched domain: ${oData.Result.name}`);
} else {
alert('No domain match');
}

View file

@ -18,7 +18,8 @@ export class AdminSettingsPackages extends AbstractViewSettings {
this.addSettings(['pluginsEnable']);
addObservablesTo(this, {
packagesError: ''
packagesError: '',
search: ''
});
this.packages = PackageAdminStore;
@ -30,6 +31,19 @@ export class AdminSettingsPackages extends AbstractViewSettings {
visibility: () => (PackageAdminStore.loading() ? 'visible' : 'hidden')
});
this.search.subscribe(value => {
const v = value.toLowerCase(),
qsa = (node, selector, fn) => node.querySelectorAll(selector).forEach(fn),
match = node => node.textContent.toLowerCase().includes(v);
if (v.length) {
qsa(this.viewModelDom, 'td:first-child', td => {
td.parentNode.hidden = !match(td);
});
} else {
qsa(this.viewModelDom, 'tr[hidden]', n => n.hidden = false);
}
});
}
onShow() {
@ -68,7 +82,7 @@ export class AdminSettingsPackages extends AbstractViewSettings {
if (iError) {
this.packagesError(
getNotification(install ? Notifications.CantInstallPackage : Notifications.CantDeletePackage)
+ (data.ErrorMessage ? ':\n' + data.ErrorMessage : '')
+ (data.message ? ':\n' + data.message : '')
);
} else if (data.Result.Reload) {
location.reload();
@ -113,8 +127,8 @@ export class AdminSettingsPackages extends AbstractViewSettings {
if (iError) {
plugin.enabled(disable);
this.packagesError(
(Notifications.UnsupportedPluginPackage === iError && data?.ErrorMessage)
? data.ErrorMessage
(Notifications.UnsupportedPluginPackage === iError && data?.message)
? data.message
: getNotification(iError)
);
}

View file

@ -10,7 +10,7 @@ export class AdminSettingsSecurity extends AbstractViewSettings {
constructor() {
super();
this.addSettings(['useLocalProxyForExternalImages', 'autoVerifySignatures']);
this.addSettings(['proxyExternalImages', 'autoVerifySignatures']);
this.weakPassword = rl.app.weakPassword;
@ -32,6 +32,8 @@ export class AdminSettingsSecurity extends AbstractViewSettings {
capaOpenPGP: SettingsCapa('OpenPGP')
});
this.gnuPGversion = 'GnuPG v' + SettingsGet('gnupg');
const reset = () => {
this.saveError(false);
this.saveSuccess(false);

View file

@ -2,7 +2,7 @@ import ko from 'ko';
//import { koComputable } from 'External/ko';
import { SettingsCapa, SettingsGet } from 'Common/Globals';
import { loadAccountsAndIdentities } from 'Common/UtilsUser';
import { loadAccountsAndIdentities, editIdentity } from 'Common/UtilsUser';
import { AccountUserStore } from 'Stores/User/Account';
import { IdentityUserStore } from 'Stores/User/Identity';
@ -12,7 +12,6 @@ import Remote from 'Remote/User/Fetch';
import { showScreenPopup } from 'Knoin/Knoin';
import { AccountPopupView } from 'View/Popup/Account';
import { IdentityPopupView } from 'View/Popup/Identity';
export class UserSettingsAccounts /*extends AbstractViewSettings*/ {
constructor() {
@ -44,11 +43,11 @@ export class UserSettingsAccounts /*extends AbstractViewSettings*/ {
}
addNewIdentity() {
showScreenPopup(IdentityPopupView);
editIdentity();
}
editIdentity(identity) {
showScreenPopup(IdentityPopupView, [identity]);
editIdentity(identity);
}
/**
@ -89,7 +88,7 @@ export class UserSettingsAccounts /*extends AbstractViewSettings*/ {
accountsAndIdentitiesAfterMove() {
Remote.request('AccountsAndIdentitiesSortOrder', null, {
Accounts: AccountUserStore.getEmailAddresses().filter(v => v != SettingsGet('mainEmail')),
Accounts: AccountUserStore.filter(item => item.isAdditional()).map(item => item.email),
Identities: IdentityUserStore.map(item => (item ? item.id() : ""))
});
}

View file

@ -2,7 +2,7 @@ import ko from 'ko';
import { koComputable } from 'External/ko';
import { SettingsGet } from 'Common/Globals';
import { i18n, translateTrigger } from 'Common/Translator';
import { i18n, translateTrigger, getErrorMessage } from 'Common/Translator';
import { ContactUserStore } from 'Stores/User/Contact';
import Remote from 'Remote/User/Fetch';
@ -15,6 +15,7 @@ export class UserSettingsContacts /*extends AbstractViewSettings*/ {
this.syncUrl = ContactUserStore.syncUrl;
this.syncUser = ContactUserStore.syncUser;
this.syncPass = ContactUserStore.syncPass;
this.syncError = ko.observable('');
this.syncModeOptions = koComputable(() => {
translateTrigger();
@ -48,4 +49,15 @@ export class UserSettingsContacts /*extends AbstractViewSettings*/ {
})
);
}
test() {
this.syncError('');
Remote.request('TestContactsSyncData', (iError, data) => {
iError && this.syncError(data.messageAdditional || data.message || getErrorMessage(iError, data));
}, {
Url: ContactUserStore.syncUrl(),
User: ContactUserStore.syncUser(),
Password: ContactUserStore.syncPass()
})
}
}

View file

@ -6,15 +6,16 @@ import { LayoutSideView, LayoutBottomView } from 'Common/EnumsUser';
import { setRefreshFoldersInterval } from 'Common/Folders';
import { Settings, SettingsGet } from 'Common/Globals';
import { WYSIWYGS } from 'Common/HtmlEditor';
import { isArray } from 'Common/Utils';
import { addSubscribablesTo, addComputablesTo } from 'External/ko';
import { i18n, translateTrigger, translatorReload, convertLangName } from 'Common/Translator';
import { editIdentity } from 'Common/UtilsUser';
import { AbstractViewSettings } from 'Knoin/AbstractViews';
import { showScreenPopup } from 'Knoin/Knoin';
import { AppUserStore } from 'Stores/User/App';
import { LanguageStore } from 'Stores/Language';
import { FolderUserStore } from 'Stores/User/Folder';
import { SettingsUserStore } from 'Stores/User/Settings';
import { IdentityUserStore } from 'Stores/User/Identity';
import { NotificationUserStore } from 'Stores/User/Notification';
@ -22,13 +23,14 @@ import { MessagelistUserStore } from 'Stores/User/Messagelist';
import Remote from 'Remote/User/Fetch';
import { IdentityPopupView } from 'View/Popup/Identity';
import { LanguagesPopupView } from 'View/Popup/Languages';
export class UserSettingsGeneral extends AbstractViewSettings {
constructor() {
super();
this.mailto = ko.observable(!!navigator.registerProtocolHandler);
this.language = LanguageStore.language;
this.languages = LanguageStore.languages;
this.hourCycle = LanguageStore.hourCycle;
@ -37,21 +39,74 @@ export class UserSettingsGeneral extends AbstractViewSettings {
this.notificationSound = ko.observable(SettingsGet('NotificationSound'));
this.notificationSounds = ko.observableArray(SettingsGet('newMailSounds'));
this.minRefreshInterval = SettingsGet('minRefreshInterval');
this.desktopNotifications = NotificationUserStore.enabled;
this.isDesktopNotificationAllowed = NotificationUserStore.allowed;
this.threadsAllowed = AppUserStore.threadsAllowed;
const i18nSort = s => i18n('MESSAGE_LIST/SORT_'+s);
this.sortSupported = FolderUserStore.hasCapability('SORT');
this.sortOptions = [
{
value: '',
text: i18nSort('DATE_DESC')
},
{
value: 'DATE',
text: i18nSort('DATE_ASC')
},
{
value: 'FROM',
text: i18nSort('FROM_ASC')
},
{
value: 'REVERSE FROM',
text: i18nSort('FROM_DESC')
},
{
value: 'REVERSE SIZE',
text: i18nSort('SIZE_DESC')
},
{
value: 'SIZE',
text: i18nSort('SIZE_ASC')
},
{
value: 'SUBJECT',
text: i18nSort('SUBJECT_ASC')
},
{
value: 'REVERSE SUBJECT',
text: i18nSort('SUBJECT_DESC')
},
{
value: 'REVERSE ARRIVAL',
text: i18nSort('ARRIVAL_DESC')
},
{
value: 'ARRIVAL',
text: i18nSort('ARRIVAL_ASC')
}
];
['useThreads',
this.threadsAllowed = AppUserStore.threadsAllowed;
// 'THREAD=REFS', 'THREAD=REFERENCES', 'THREAD=ORDEREDSUBJECT'
this.threadAlgorithms = ko.observableArray();
FolderUserStore.capabilities.forEach(capa =>
capa.startsWith('THREAD=') && this.threadAlgorithms.push(capa.slice(7))
);
this.threadAlgorithms.sort((a, b) => a.length - b.length);
['defaultSort', 'useThreads', 'threadAlgorithm',
// These use addSetting()
'layout', 'messageReadDelay', 'messagesPerPage', 'checkMailInterval',
'editorDefaultType', 'editorWysiwyg', 'msgDefaultAction', 'maxBlockquotesLevel',
'editorDefaultType', 'editorWysiwyg', 'msgDefaultAction', 'maxBlockquotesLevel', 'minFontSize',
// These are in addSettings()
'requestReadReceipt', 'requestDsn', 'requireTLS', 'pgpSign', 'pgpEncrypt',
'viewHTML', 'viewImages', 'viewImagesWhitelist', 'removeColors', 'allowStyles', 'allowDraftAutosave',
'hideDeleted', 'listInlineAttachments', 'simpleAttachmentsList', 'collapseBlockquotes',
'useCheckboxesInList', 'listGrouped', 'replySameFolder', 'allowSpellcheck',
'messageReadAuto', 'showNextMessage', 'messageNewWindow'
'messageReadAuto', 'showNextMessage', 'messageNewWindow', 'markdown'
].forEach(name => this[name] = SettingsUserStore[name]);
this.allowLanguagesOnSettings = !!SettingsGet('allowLanguagesOnSettings');
@ -65,14 +120,9 @@ export class UserSettingsGeneral extends AbstractViewSettings {
addComputablesTo(this, {
languageFullName: () => convertLangName(this.language()),
identityMain: () => {
const list = this.identities();
return isArray(list) ? list.find(item => item && !item.id()) : null;
},
identityMainDesc: () => {
const identity = this.identityMain();
return identity ? identity.formattedName() : '---';
const identity = IdentityUserStore.main();
return identity ? identity.toString() : '---';
},
editorDefaultTypes: () => {
@ -111,13 +161,15 @@ export class UserSettingsGeneral extends AbstractViewSettings {
this.addSetting('CheckMailInterval');
this.addSetting('Layout');
this.addSetting('MaxBlockquotesLevel');
this.addSetting('minFontSize');
this.addSetting('defaultSort');
this.addSettings([
'requestReadReceipt', 'requestDsn', 'requireTLS', 'pgpSign', 'pgpEncrypt',
'ViewHTML', 'ViewImages', 'ViewImagesWhitelist', 'RemoveColors', 'AllowStyles', 'AllowDraftAutosave',
'HideDeleted', 'ListInlineAttachments', 'simpleAttachmentsList', 'CollapseBlockquotes',
'UseCheckboxesInList', 'listGrouped', 'ReplySameFolder', 'allowSpellcheck',
'messageReadAuto', 'showNextMessage', 'messageNewWindow',
'messageReadAuto', 'showNextMessage', 'messageNewWindow', 'markdown',
'DesktopNotifications', 'SoundNotification']);
const fReloadLanguageHelper = (saveSettingsStep) => () => {
@ -146,6 +198,11 @@ export class UserSettingsGeneral extends AbstractViewSettings {
Remote.saveSetting('UseThreads', value);
},
threadAlgorithm: value => {
MessagelistUserStore([]);
Remote.saveSetting('threadAlgorithm', value);
},
checkMailInterval: () => {
setRefreshFoldersInterval(SettingsUserStore.checkMailInterval());
}
@ -153,8 +210,7 @@ export class UserSettingsGeneral extends AbstractViewSettings {
}
editMainIdentity() {
const identity = this.identityMain();
identity && showScreenPopup(IdentityPopupView, [identity]);
editIdentity(IdentityUserStore.main());
}
testSoundNotification() {
@ -168,4 +224,15 @@ export class UserSettingsGeneral extends AbstractViewSettings {
selectLanguage() {
showScreenPopup(LanguagesPopupView, [this.language, this.languages(), LanguageStore.userLanguage()]);
}
registerMailto() {
console.log(`mailto = ${location.protocol}//${location.host}${location.pathname}?mailto`);
navigator.registerProtocolHandler(
'mailto',
`${location.protocol}//${location.host}${location.pathname}?mailto&to=%s`,
(SettingsGet('title') || 'SnappyMail')
);
alert(i18n('GLOBAL/DONE'));
this.mailto(0);
}
}

View file

@ -18,8 +18,6 @@ import { OpenPgpGeneratePopupView } from 'View/Popup/OpenPgpGenerate';
import { SMimeUserStore } from 'Stores/User/SMime';
import { SMimeImportPopupView } from 'View/Popup/SMimeImport';
import Remote from 'Remote/User/Fetch';
export class UserSettingsSecurity extends AbstractViewSettings {
constructor() {
super();
@ -28,9 +26,9 @@ export class UserSettingsSecurity extends AbstractViewSettings {
this.autoLogoutOptions = koComputable(() => {
translateTrigger();
return [
{ id: 0, name: i18n('SETTINGS_SECURITY/AUTOLOGIN_NEVER_OPTION_NAME') },
{ id: 0, name: i18n('SETTINGS_SECURITY/NEVER') },
{ id: 5, name: relativeTime(300) },
{ id: 10, name: relativeTime(600) },
{ id: 15, name: relativeTime(900) },
{ id: 30, name: relativeTime(1800) },
{ id: 60, name: relativeTime(3600) },
{ id: 120, name: relativeTime(7200) },
@ -40,6 +38,9 @@ export class UserSettingsSecurity extends AbstractViewSettings {
});
this.addSetting('AutoLogout');
this.keyPassForget = SettingsUserStore.keyPassForget;
this.addSetting('keyPassForget');
this.gnupgPublicKeys = GnuPGUserStore.publicKeys;
this.gnupgPrivateKeys = GnuPGUserStore.privateKeys;
@ -62,9 +63,7 @@ export class UserSettingsSecurity extends AbstractViewSettings {
}
importToOpenPGP() {
OpenPGPUserStore.isSupported() && Remote.request('GetPGPKeys',
(iError, oData) => !iError && oData.Result && OpenPGPUserStore.importKeys(oData.Result)
);
OpenPGPUserStore.loadBackupKeys();
}
importToSMime() {

View file

@ -99,8 +99,8 @@ export class UserSettingsThemes /*extends AbstractViewSettings*/ {
themeBackground.hash(data?.Result?.hash || '');
if (!themeBackground.name() || !themeBackground.hash()) {
let errorMsg = '';
if (data.ErrorCode) {
switch (data.ErrorCode) {
if (data.code) {
switch (data.code) {
case UploadErrorCode.FileIsTooBig:
errorMsg = i18n('SETTINGS_THEMES/ERROR_FILE_IS_TOO_BIG');
break;
@ -111,7 +111,7 @@ export class UserSettingsThemes /*extends AbstractViewSettings*/ {
}
}
themeBackground.error(errorMsg || data.ErrorMessage || i18n('SETTINGS_THEMES/ERROR_UNKNOWN'));
themeBackground.error(errorMsg || data.message || i18n('SETTINGS_THEMES/ERROR_UNKNOWN'));
}
});
}

View file

@ -15,7 +15,6 @@ import {
} from 'Sieve/Commands/Actions';
import {
ConditionalCommand,
ElsIfCommand,
ElseCommand,
IfCommand,
@ -49,6 +48,7 @@ import { NotifyCommand, ValidNotifyMethodTest, NotifyMethodCapabilityTest } from
import { IHaveTest, ErrorCommand } from 'Sieve/Extensions/rfc5463';
import { MailboxExistsTest, MetadataTest, MetadataExistsTest } from 'Sieve/Extensions/rfc5490';
import { ForEveryPartCommand, BreakCommand, ReplaceCommand, EncloseCommand, ExtractTextCommand } from 'Sieve/Extensions/rfc5703';
import { ValidExtListTest } from 'Sieve/Extensions/rfc6134';
import { IncludeCommand, ReturnCommand, GlobalCommand } from 'Sieve/Extensions/rfc6609';
export const
@ -67,7 +67,6 @@ export const
IfCommand,
ElsIfCommand,
ElseCommand,
ConditionalCommand,
RequireCommand,
StopCommand,
// Action commands
@ -129,6 +128,8 @@ export const
ReplaceCommand,
EncloseCommand,
ExtractTextCommand,
// rfc6134
ValidExtListTest,
// rfc6609
IncludeCommand,
ReturnCommand,
@ -146,6 +147,17 @@ export const
return commands;
},
unavailableCommands = () => {
let commands = {};
AllCommands.forEach(cmd => {
const obj = new cmd, requires = obj.require;
if (requires && !(Array.isArray(requires) ? requires : [requires]).every(string => capa.includes(string))) {
commands[obj.identifier] = cmd;
}
});
return commands;
},
availableActions = () => {
let actions = {}, id;
AllCommands.forEach(cmd => {

View file

@ -21,6 +21,10 @@ export class FileIntoCommand extends ActionCommand
super();
// QuotedString / MultiLine
this._mailbox = new GrammarQuotedString();
// https://datatracker.ietf.org/doc/html/rfc3894
this.copy = false;
// https://datatracker.ietf.org/doc/html/rfc5490#section-3.2
this.create = false;
}
get require() { return 'fileinto'; }
@ -28,9 +32,7 @@ export class FileIntoCommand extends ActionCommand
toString()
{
return 'fileinto'
// https://datatracker.ietf.org/doc/html/rfc3894
+ ((this.copy && capa.includes('copy')) ? ' :copy' : '')
// https://datatracker.ietf.org/doc/html/rfc5490#section-3.2
+ ((this.create && capa.includes('mailbox')) ? ' :create' : '')
+ ' ' + this._mailbox
+ ';';
@ -64,15 +66,17 @@ export class RedirectCommand extends ActionCommand
super();
// QuotedString / MultiLine
this._address = new GrammarQuotedString();
// https://datatracker.ietf.org/doc/html/rfc3894
this.copy = false;
// https://datatracker.ietf.org/doc/html/rfc6134#section-2.3
this.list = null;
}
toString()
{
return 'redirect'
// https://datatracker.ietf.org/doc/html/rfc6134#section-2.3
// + ((this.list && capa.includes('extlists')) ? ' :list ' + this.list : '')
// https://datatracker.ietf.org/doc/html/rfc3894
+ ((this.copy && capa.includes('copy')) ? ' :copy' : '')
+ ' ' + this._address
+ ';';

View file

@ -5,6 +5,7 @@
import {
ControlCommand,
GrammarCommands,
GrammarStringList,
GrammarQuotedString
} from 'Sieve/Grammar';
@ -16,37 +17,55 @@ import {
* elsif <test2: test> <block2: block>
* else <block3: block>
*/
export class ConditionalCommand extends ControlCommand
export /*abstract*/ class ConditionalCommand extends ControlCommand
{
constructor(identifier)
constructor()
{
super(identifier);
this.test = null;
}
toString()
{
return this.identifier + ' ' + this.test + ' ' + this.commands;
}
/*
public function pushArguments(array $args): void
{
args.forEach((arg, i) => {
if (i && ':' === args[i-1][0]) {
this[args[i-1].replace(':','_')].value = arg.value;
}
});
print_r($args);
exit;
}
if (this.constructor == ConditionalCommand) {
throw Error("Abstract class can't be instantiated.");
}
*/
super();
this.commands = new GrammarCommands;
}
}
export class IfCommand extends ConditionalCommand
{
constructor()
{
super();
this._test = null; // must be descendent instanceof TestCommand
}
get test()
{
return this._test;
}
set test(value)
{
/*
if (!value instanceof TestCommand) {
throw Error("test must be descendent instanceof TestCommand.");
}
*/
this._test = value;
}
toString()
{
/*
if (!this._test instanceof TestCommand) {
throw Error("test must be descendent instanceof TestCommand.");
}
*/
return this.identifier + ' ' + this._test + ' ' + this.commands;
}
}
export class ElsIfCommand extends ConditionalCommand
export class ElsIfCommand extends IfCommand
{
}

View file

@ -40,8 +40,8 @@ export class AddressTest extends TestCommand
this.header_list = new GrammarStringList;
this.key_list = new GrammarStringList;
// rfc5260#section-6
// this.index = new GrammarNumber;
// this.last = false;
this.index = new GrammarNumber('');
this.last = false;
// rfc5703#section-6
// this.mime
// this.anychild
@ -67,10 +67,11 @@ export class AddressTest extends TestCommand
}
}
return result
// + (this.last ? ' :last' : (this.index.value ? ' :index ' + this.index : ''))
+ (this.comparator ? ' :comparator ' + this.comparator : '')
+ (this.last ? ' :last' : (this.index.value ? ' :index ' + this.index : ''))
+ (this._comparator ? ' :comparator ' + this._comparator : '')
+ ' ' + this.address_part
+ ' ' + this.match_type
+ (this._match_type ? ' ' + this._match_type : '')
+ (this.relational_match ? ' ' + this.relational_match : '')
+ ' ' + this.header_list
+ ' ' + this.key_list;
}
@ -147,9 +148,10 @@ export class EnvelopeTest extends TestCommand
toString()
{
return 'envelope'
+ (this.comparator ? ' :comparator ' + this.comparator : '')
+ (this._comparator ? ' :comparator ' + this._comparator : '')
+ ' ' + this.address_part
+ ' ' + this.match_type
+ (this._match_type ? ' ' + this._match_type : '')
+ (this.relational_match ? ' ' + this.relational_match : '')
+ ' ' + this.envelope_part
+ ' ' + this.key_list;
}
@ -234,8 +236,8 @@ export class HeaderTest extends TestCommand
this.header_names = new GrammarStringList;
this.key_list = new GrammarStringList;
// rfc5260#section-6
// this.index = new GrammarNumber;
// this.last = false;
this.index = new GrammarNumber('');
this.last = false;
// rfc5703#section-6
this.mime = false;
this.anychild = false;
@ -278,9 +280,10 @@ export class HeaderTest extends TestCommand
}
}
return result
// + (this.last ? ' :last' : (this.index.value ? ' :index ' + this.index : ''))
+ (this.comparator ? ' :comparator ' + this.comparator : '')
+ ' ' + this.match_type
+ (this.last ? ' :last' : (this.index.value ? ' :index ' + this.index : ''))
+ (this._comparator ? ' :comparator ' + this._comparator : '')
+ (this._match_type ? ' ' + this._match_type : '')
+ (this.relational_match ? ' ' + this.relational_match : '')
+ ' ' + this.header_names
+ ' ' + this.key_list;
}
@ -316,11 +319,6 @@ export class NotTest extends TestCommand
{
return 'not ' + this.test;
}
pushArguments()
{
throw 'No arguments';
}
}
/**

View file

@ -22,8 +22,9 @@ export class BodyTest extends TestCommand
toString()
{
return 'body'
+ (this.comparator ? ' :comparator ' + this.comparator : '')
+ ' ' + this.match_type
+ (this._comparator ? ' :comparator ' + this._comparator : '')
+ (this._match_type ? ' ' + this._match_type : '')
+ (this.relational_match ? ' ' + this.relational_match : '')
+ ' ' + this.body_transform
+ ' ' + this.key_list;
}

View file

@ -13,24 +13,28 @@ export class EnvironmentTest extends TestCommand
constructor()
{
super();
this.name = new GrammarQuotedString;
this._name = new GrammarQuotedString;
this.key_list = new GrammarStringList;
}
get name() { return this._name.value; }
set name(v) { this._name.value = v; }
get require() { return 'environment'; }
toString()
{
return 'environment'
+ (this.comparator ? ' :comparator ' + this.comparator : '')
+ ' ' + this.match_type
+ ' ' + this.name
+ (this._comparator ? ' :comparator ' + this._comparator : '')
+ (this._match_type ? ' ' + this._match_type : '')
+ (this.relational_match ? ' ' + this.relational_match : '')
+ ' ' + this._name
+ ' ' + this.key_list;
}
pushArguments(args)
{
this.key_list = args.pop();
this.name = args.pop();
this._name = args.pop();
}
}

View file

@ -21,6 +21,12 @@ export class SetCommand extends ActionCommand
get require() { return 'variables'; }
get name() { return this._name.value; }
set name(str) { this._name.value = str; }
get value() { return this._value.value; }
set value(str) { this._value.value = str; }
toString()
{
return 'set'
@ -29,12 +35,6 @@ export class SetCommand extends ActionCommand
+ ' ' + this._value;
}
get name() { return this._name.value; }
set name(str) { this._name.value = str; }
get value() { return this._value.value; }
set value(str) { this._value.value = str; }
pushArguments(args)
{
[':lower', ':upper', ':lowerfirst', ':upperfirst', ':quotewildcard', ':length'].forEach(modifier => {
@ -57,8 +57,9 @@ export class StringTest extends TestCommand
toString()
{
return 'string'
+ ' ' + this.match_type
+ (this.comparator ? ' :comparator ' + this.comparator : '')
+ (this._match_type ? ' ' + this._match_type : '')
+ (this.relational_match ? ' ' + this.relational_match : '')
+ (this._comparator ? ' :comparator ' + this._comparator : '')
+ ' ' + this.source
+ ' ' + this.key_list;
}

View file

@ -30,33 +30,6 @@ export class VacationCommand extends ActionCommand
// get require() { return ['vacation','vacation-seconds']; }
get require() { return 'vacation'; }
toString()
{
let result = 'vacation';
if (0 < this._seconds.value && capa.includes('vacation-seconds')) {
result += ' :seconds ' + this._seconds;
} else if (0 < this._days.value) {
result += ' :days ' + this._days;
}
if (this._subject.length) {
result += ' :subject ' + this._subject;
}
if (this._from.length) {
result += ' :from ' + this._from;
// result += ' :from ' + this.arguments[':from'];
}
if (this.addresses.length) {
result += ' :addresses ' + this.addresses;
}
if (this.mime) {
result += ' :mime';
}
if (this._handle.length) {
result += ' :handle ' + this._handle;
}
return result + ' ' + this._reason;
}
get days() { return this._days.value; }
get seconds() { return this._seconds.value; }
get subject() { return this._subject.value; }
@ -71,6 +44,32 @@ export class VacationCommand extends ActionCommand
set handle(str) { this._handle.value = str; }
set reason(str) { this._reason.value = str; }
toString()
{
let result = 'vacation';
if (0 < this._seconds.value && capa.includes('vacation-seconds')) {
result += ' :seconds ' + this._seconds;
} else if (0 < this._days.value) {
result += ' :days ' + this._days;
}
if (this._subject.length) {
result += ' :subject ' + this._subject;
}
if (this._from.length) {
result += ' :from ' + this._from;
}
if (this.addresses.length) {
result += ' :addresses ' + this.addresses;
}
if (this.mime) {
result += ' :mime';
}
if (this._handle.length) {
result += ' :handle ' + this._handle;
}
return result + ' ' + this._reason;
}
pushArguments(args)
{
this._reason.value = args.pop().value; // GrammarQuotedString
@ -81,7 +80,8 @@ export class VacationCommand extends ActionCommand
this.addresses = arg; // GrammarStringList
} else if (i && ':' === args[i-1][0]) {
// :days, :seconds, :subject, :from, :handle
this[args[i-1].replace(':','_')].value = arg.value;
let p = args[i-1].replace(':','_');
this[p] ? (this[p].value = arg.value) : console.log('Unknown VacationCommand :' + p);
}
});
}

View file

@ -43,11 +43,12 @@ class FlagCommand extends ActionCommand
if (args[0] instanceof GrammarQuotedString) {
this._variablename = args[0];
}
if (args[1] instanceof GrammarString) {
this.list_of_flags = args[1];
}
} else if (args[0] instanceof GrammarString) {
args[0] = args[1];
}
if (args[0] instanceof GrammarStringList) {
this.list_of_flags = args[0];
} else if (args[0]) {
this.list_of_flags.push(args[0]);
}
}
}
@ -78,8 +79,9 @@ export class HasFlagTest extends TestCommand
toString()
{
return 'hasflag'
+ ' ' + this.match_type
+ (this.comparator ? ' :comparator ' + this.comparator : '')
+ (this._match_type ? ' ' + this._match_type : '')
+ (this.relational_match ? ' ' + this.relational_match : '')
+ (this._comparator ? ' :comparator ' + this._comparator : '')
+ ' ' + this.variable_list
+ ' ' + this.list_of_flags;
}

View file

@ -4,7 +4,6 @@
import {
GrammarQuotedString,
GrammarString,
TestCommand
} from 'Sieve/Grammar';
@ -14,19 +13,23 @@ export class SpamTestTest extends TestCommand
{
super();
this.percent = false, // 0 - 100 else 0 - 10
this.value = new GrammarQuotedString;
this._value = new GrammarQuotedString;
}
// get require() { return this.percent ? 'spamtestplus' : 'spamtest'; }
get require() { return /:value|:count/.test(this.match_type) ? ['spamtestplus','relational'] : 'spamtestplus'; }
get require() { return /:value|:count/.test(this._match_type) ? ['spamtestplus','relational'] : 'spamtestplus'; }
get value() { return this._value.value; }
set value(v) { this._value.value = v; }
toString()
{
return 'spamtest'
+ (this.percent ? ' :percent' : '')
+ (this.comparator ? ' :comparator ' + this.comparator : '')
+ ' ' + this.match_type
+ ' ' + this.value;
+ (this._comparator ? ' :comparator ' + this._comparator : '')
+ (this._match_type ? ' ' + this._match_type : '')
+ (this.relational_match ? ' ' + this.relational_match : '')
+ ' ' + this._value;
}
pushArguments(args)
@ -34,8 +37,8 @@ export class SpamTestTest extends TestCommand
args.forEach(arg => {
if (':percent' === arg) {
this.percent = true;
} else if (arg instanceof GrammarString) {
this.value = arg;
} else if (arg instanceof GrammarQuotedString) {
this._value = arg;
}
});
}
@ -46,24 +49,28 @@ export class VirusTestTest extends TestCommand
constructor()
{
super();
this.value = new GrammarQuotedString; // 1 - 5
this._value = new GrammarQuotedString; // 1 - 5
}
get require() { return /:value|:count/.test(this.match_type) ? ['virustest','relational'] : 'virustest'; }
get require() { return ':value' == this._match_type ? ['virustest','relational'] : 'virustest'; }
get value() { return this._value.value; }
set value(v) { this._value.value = v; }
toString()
{
return 'virustest'
+ (this.comparator ? ' :comparator ' + this.comparator : '')
+ ' ' + this.match_type
+ ' ' + this.value;
+ (this._comparator ? ' :comparator ' + this._comparator : '')
+ (this._match_type ? ' ' + this._match_type : '')
+ (this.relational_match ? ' ' + this.relational_match : '')
+ ' ' + this._value;
}
pushArguments(args)
{
args.forEach(arg => {
if (arg instanceof GrammarString) {
this.value = arg;
if (arg instanceof GrammarQuotedString) {
this._value = arg;
}
});
}

View file

@ -14,10 +14,10 @@ export class DateTest extends TestCommand
constructor()
{
super();
this.zone = new GrammarQuotedString;
this._zone = new GrammarQuotedString;
this.originalzone = false;
this.header_name = new GrammarQuotedString;
this.date_part = new GrammarQuotedString;
this._header_name = new GrammarQuotedString;
this._date_part = new GrammarQuotedString;
this.key_list = new GrammarStringList;
// rfc5260#section-6
this.index = new GrammarNumber;
@ -27,30 +27,40 @@ export class DateTest extends TestCommand
// get require() { return ['date','index']; }
get require() { return 'date'; }
get zone() { return this._zone.value; }
set zone(v) { this._zone.value = v; }
get header_name() { return this._header_name.value; }
set header_name(v) { this._header_name.value = v; }
get date_part() { return this._date_part.value; }
set date_part(v) { this._date_part.value = v; }
toString()
{
return 'date'
+ (this.last ? ' :last' : (this.index.value ? ' :index ' + this.index : ''))
+ (this.originalzone ? ' :originalzone' : (this.zone.length ? ' :zone ' + this.zone : ''))
+ (this.comparator ? ' :comparator ' + this.comparator : '')
+ ' ' + this.match_type
+ ' ' + this.header_name
+ ' ' + this.date_part
+ (this.originalzone ? ' :originalzone' : (this._zone.length ? ' :zone ' + this._zone : ''))
+ (this._comparator ? ' :comparator ' + this._comparator : '')
+ (this._match_type ? ' ' + this._match_type : '')
+ (this.relational_match ? ' ' + this.relational_match : '')
+ ' ' + this._header_name
+ ' ' + this._date_part
+ ' ' + this.key_list;
}
pushArguments(args)
{
this.key_list = args.pop();
this.date_part = args.pop();
this.header_name = args.pop();
this._date_part = args.pop();
this._header_name = args.pop();
args.forEach((arg, i) => {
if (':originalzone' === arg) {
this.originalzone = true;
} else if (':last' === arg) {
this.last = true;
} else if (i && ':zone' === args[i-1]) {
this.zone.value = arg.value;
this._zone.value = arg.value;
} else if (i && ':index' === args[i-1]) {
this.index.value = arg.value;
}
@ -63,30 +73,37 @@ export class CurrentDateTest extends TestCommand
constructor()
{
super();
this.zone = new GrammarQuotedString;
this.date_part = new GrammarQuotedString;
this._zone = new GrammarQuotedString;
this._date_part = new GrammarQuotedString;
this.key_list = new GrammarStringList;
}
get require() { return 'date'; }
get zone() { return this._zone.value; }
set zone(v) { this._zone.value = v; }
get date_part() { return this._date_part.value; }
set date_part(v) { this._date_part.value = v; }
toString()
{
return 'currentdate'
+ (this.zone.length ? ' :zone ' + this.zone : '')
+ (this.comparator ? ' :comparator ' + this.comparator : '')
+ ' ' + this.match_type
+ ' ' + this.date_part
+ (this._zone.length ? ' :zone ' + this._zone : '')
+ (this._comparator ? ' :comparator ' + this._comparator : '')
+ (this._match_type ? ' ' + this._match_type : '')
+ (this.relational_match ? ' ' + this.relational_match : '')
+ ' ' + this._date_part
+ ' ' + this.key_list;
}
pushArguments(args)
{
this.key_list = args.pop();
this.date_part = args.pop();
this._date_part = args.pop();
args.forEach((arg, i) => {
if (i && ':zone' === args[i-1]) {
this.zone.value = arg.value;
this._zone.value = arg.value;
}
});
}

View file

@ -16,24 +16,30 @@ export class AddHeaderCommand extends ActionCommand
{
super();
this.last = false;
this.field_name = new GrammarQuotedString;
this.value = new GrammarQuotedString;
this._field_name = new GrammarQuotedString;
this._value = new GrammarQuotedString;
}
get require() { return 'editheader'; }
get field_name() { return this._field_name.value; }
set field_name(v) { this._field_name.value = v; }
get value() { return this._value.value; }
set value(v) { this._value.value = v; }
toString()
{
return this.identifier
+ (this.last ? ' :last' : '')
+ ' ' + this.field_name
+ ' ' + this.value + ';';
+ ' ' + this._field_name
+ ' ' + this._value + ';';
}
pushArguments(args)
{
this.value = args.pop();
this.field_name = args.pop();
this._value = args.pop();
this._field_name = args.pop();
this.last = args.includes(':last');
}
}
@ -47,19 +53,22 @@ export class DeleteHeaderCommand extends ActionCommand
this.last = false;
this.comparator = '',
this.match_type = ':is',
this.field_name = new GrammarQuotedString;
this._field_name = new GrammarQuotedString;
this.value_patterns = new GrammarStringList;
}
get require() { return 'editheader'; }
get field_name() { return this._field_name.value; }
set field_name(v) { this._field_name.value = v; }
toString()
{
return this.identifier
+ (this.last ? ' :last' : (this.index.value ? ' :index ' + this.index : ''))
+ (this.comparator ? ' :comparator ' + this.comparator : '')
+ ' ' + this.match_type
+ ' ' + this.field_name
+ ' ' + this._field_name
+ ' ' + this.value_patterns + ';';
}
@ -76,10 +85,10 @@ export class DeleteHeaderCommand extends ActionCommand
});
if (l && args[l-1] instanceof GrammarString) {
this.field_name = args[l-1];
this._field_name = args[l-1];
this.value_patterns = args[l];
} else {
this.field_name = args[l];
this._field_name = args[l];
}
}
}

View file

@ -8,11 +8,11 @@ import {
GrammarString
} from 'Sieve/Grammar';
class rfc5429Command extends ActionCommand
class /*abstract*/ rfc5429Command extends ActionCommand
{
constructor(identifier)
constructor()
{
super(identifier);
super();
this._reason = new GrammarQuotedString;
}
@ -44,7 +44,6 @@ class rfc5429Command extends ActionCommand
*/
export class ErejectCommand extends rfc5429Command
{
constructor() { super('ereject'); }
get require() { return 'ereject'; }
}
@ -53,6 +52,5 @@ export class ErejectCommand extends rfc5429Command
*/
export class RejectCommand extends rfc5429Command
{
constructor() { super('reject'); }
get require() { return 'reject'; }
}

View file

@ -26,14 +26,16 @@ export class NotifyCommand extends ActionCommand
}
get method() { return this._method.value; }
get from() { return this._from.value; }
get importance() { return this._importance.value; }
get message() { return this._message.value; }
set method(str) { this._method.value = str; }
set method(str) { this._method.value = str; }
set from(str) { this._from.value = str; }
get from() { return this._from.value; }
set from(str) { this._from.value = str; }
get importance() { return this._importance.value; }
set importance(int) { this._importance.value = int; }
set message(str) { this._message.value = str; }
get message() { return this._message.value; }
set message(str) { this._message.value = str; }
get require() { return 'enotify'; }
@ -63,7 +65,8 @@ export class NotifyCommand extends ActionCommand
this.options = arg; // GrammarStringList
} else if (i && ':' === args[i-1][0]) {
// :from, :importance, :message
this[args[i-1].replace(':','_')].value = arg.value;
let p = args[i-1].replace(':','_');
this[p] ? (this[p].value = arg.value) : console.log('Unknown VacationCommand :' + p);
}
});
}
@ -99,25 +102,32 @@ export class NotifyMethodCapabilityTest extends TestCommand
constructor()
{
super();
this.notification_uri = new GrammarQuotedString;
this.notification_capability = new GrammarQuotedString;
this._notification_uri = new GrammarQuotedString;
this._notification_capability = new GrammarQuotedString;
this.key_list = new GrammarStringList;
}
get notification_uri() { return this._notification_uri.value; }
set notification_uri(v) { this._notification_uri.value = v; }
get notification_capability() { return this._notification_capability.value; }
set notification_capability(v) { this._notification_capability.value = v; }
toString()
{
return 'valid_notify_method '
+ (this.comparator ? ' :comparator ' + this.comparator : '')
+ (this.match_type ? ' ' + this.match_type : '')
+ this.notification_uri
+ this.notification_capability
+ (this._comparator ? ' :comparator ' + this._comparator : '')
+ (this._match_type ? ' ' + this._match_type : '')
+ (this.relational_match ? ' ' + this.relational_match : '')
+ this._notification_uri
+ this._notification_capability
+ this.key_list;
}
pushArguments(args)
{
this.key_list = args.pop();
this.notification_capability = args.pop();
this.notification_uri = args.pop();
this._notification_capability = args.pop();
this._notification_uri = args.pop();
}
}

View file

@ -41,18 +41,21 @@ export class ErrorCommand extends ControlCommand
constructor()
{
super();
this.message = new GrammarQuotedString;
this._message = new GrammarQuotedString;
}
get require() { return 'ihave'; }
get message() { return this._message.value; }
set message(v) { this._message.value = v; }
toString()
{
return 'error ' + this.message + ';';
return 'error ' + this._message + ';';
}
pushArguments(args)
{
this.message = args.pop();
this._message = args.pop();
}
}

View file

@ -42,28 +42,35 @@ export class MetadataTest extends TestCommand
constructor()
{
super();
this.mailbox = new GrammarQuotedString;
this.annotation_name = new GrammarQuotedString;
this._mailbox = new GrammarQuotedString;
this._annotation_name = new GrammarQuotedString;
this.key_list = new GrammarStringList;
}
get require() { return 'mboxmetadata'; }
get mailbox() { return this._mailbox.value; }
set mailbox(v) { this._mailbox.value = v; }
get annotation_name() { return this._annotation_name.value; }
set annotation_name(v) { this._annotation_name.value = v; }
toString()
{
return 'metadata '
+ ' ' + this.match_type
+ (this.comparator ? ' :comparator ' + this.comparator : '')
+ ' ' + this.mailbox
+ ' ' + this.annotation_name
+ ' ' + this._match_type
+ (this.relational_match ? ' ' + this.relational_match : '')
+ (this._comparator ? ' :comparator ' + this._comparator : '')
+ ' ' + this._mailbox
+ ' ' + this._annotation_name
+ ' ' + this.key_list;
}
pushArguments(args)
{
this.key_list = args.pop();
this.annotation_name = args.pop();
this.mailbox = args.pop();
this._annotation_name = args.pop();
this._mailbox = args.pop();
}
}
@ -75,23 +82,26 @@ export class MetadataExistsTest extends TestCommand
constructor()
{
super();
this.mailbox = new GrammarQuotedString;
this._mailbox = new GrammarQuotedString;
this.annotation_names = new GrammarStringList;
}
get require() { return 'mboxmetadata'; }
get mailbox() { return this._mailbox.value; }
set mailbox(v) { this._mailbox.value = v; }
toString()
{
return 'metadataexists '
+ ' ' + this.mailbox
+ ' ' + this._mailbox
+ ' ' + this.annotation_names;
}
pushArguments(args)
{
this.annotation_names = args.pop();
this.mailbox = args.pop();
this._mailbox = args.pop();
}
}
@ -103,25 +113,29 @@ export class ServerMetadataTest extends TestCommand
constructor()
{
super();
this.annotation_name = new GrammarQuotedString;
this._annotation_name = new GrammarQuotedString;
this.key_list = new GrammarStringList;
}
get require() { return 'servermetadata'; }
get annotation_name() { return this._annotation_name.value; }
set annotation_name(v) { this._annotation_name.value = v; }
toString()
{
return 'servermetadata '
+ ' ' + this.match_type
+ (this.comparator ? ' :comparator ' + this.comparator : '')
+ ' ' + this.annotation_name
+ ' ' + this._match_type
+ (this.relational_match ? ' ' + this.relational_match : '')
+ (this._comparator ? ' :comparator ' + this._comparator : '')
+ ' ' + this._annotation_name
+ ' ' + this.key_list;
}
pushArguments(args)
{
this.key_list = args.pop();
this.annotation_name = args.pop();
this._annotation_name = args.pop();
}
}

View file

@ -5,6 +5,7 @@
import {
ActionCommand,
ControlCommand,
GrammarCommands,
GrammarNumber,
GrammarQuotedString,
GrammarString,
@ -19,7 +20,8 @@ export class ForEveryPartCommand extends ControlCommand
constructor()
{
super();
this._name = new GrammarString;
this.name = new GrammarString;
this.commands = new GrammarCommands;
}
get require() { return 'foreverypart'; }
@ -27,8 +29,8 @@ export class ForEveryPartCommand extends ControlCommand
toString()
{
let result = 'foreverypart';
if (this._subject.length) {
result += ' :name ' + this._name;
if (this.name.length) {
result += ' :name ' + this.name;
}
return result + ' ' + this.commands;
}
@ -37,19 +39,22 @@ export class ForEveryPartCommand extends ControlCommand
{
args.forEach((arg, i) => {
if (':name' === arg) {
this._name.value = args[i+1].value;
this.name.value = args[i+1].value;
}
});
}
}
/**
* Must be inside foreverypart
*/
export class BreakCommand extends ForEveryPartCommand
{
toString()
{
let result = 'break';
if (this._subject.length) {
result += ' :name ' + this._name;
if (this.name.length) {
result += ' :name ' + this.name;
}
return result + ';';
}
@ -63,14 +68,23 @@ export class ReplaceCommand extends ActionCommand
constructor()
{
super();
this.mime = false;
this._subject = new GrammarQuotedString;
this._from = new GrammarQuotedString;
this.replacement = new GrammarQuotedString;
this.mime = false;
this._subject = new GrammarQuotedString;
this._from = new GrammarQuotedString;
this._replacement = new GrammarQuotedString;
}
get require() { return 'replace'; }
get subject() { return this._subject.value; }
set subject(str) { this._subject.value = str; }
get from() { return this._from.value; }
set from(str) { this._from.value = str; }
get replacement() { return this._replacement.value; }
set replacement(str) { this._replacement.value = str; }
toString()
{
let result = 'replace';
@ -82,20 +96,20 @@ export class ReplaceCommand extends ActionCommand
}
if (this._from.length) {
result += ' :from ' + this._from;
// result += ' :from ' + this.arguments[':from'];
}
return result + this.replacement + ';';
return result + this._replacement + ';';
}
pushArguments(args)
{
this.replacement = args.pop();
this._replacement = args.pop();
args.forEach((arg, i) => {
if (':mime' === arg) {
this.mime = true;
} else if (i && ':' === args[i-1][0]) {
// :subject, :from
this[args[i-1].replace(':','_')].value = arg.value;
let p = args[i-1].replace(':','_');
this[p] ? (this[p].value = arg.value) : console.log('Unknown VacationCommand :' + p);
}
});
}
@ -115,6 +129,9 @@ export class EncloseCommand extends ActionCommand
get require() { return 'enclose'; }
get subject() { return this._subject.value; }
set subject(v) { this._subject.value = v; }
toString()
{
let result = 'enclose';
@ -132,7 +149,8 @@ export class EncloseCommand extends ActionCommand
args.forEach((arg, i) => {
if (i && ':' === args[i-1][0]) {
// :subject, :headers
this[args[i-1].replace(':','_')].value = arg.value;
let p = args[i-1].replace(':','_');
this[p] ? (this[p].value = arg.value) : console.log('Unknown VacationCommand :' + p);
}
});
}
@ -140,6 +158,7 @@ export class EncloseCommand extends ActionCommand
/**
* https://datatracker.ietf.org/doc/html/rfc5703#section-7
* Should be inside foreverypart, else empty and flagged as a compilation error
*/
export class ExtractTextCommand extends ActionCommand
{
@ -148,9 +167,12 @@ export class ExtractTextCommand extends ActionCommand
super();
this.modifiers = [];
this._first = new GrammarNumber;
this.varname = new GrammarQuotedString;
this._varname = new GrammarQuotedString;
}
get varname() { return this._varname.value; }
set varname(v) { this._varname.value = v; }
get require() { return 'extracttext'; }
toString()
@ -160,12 +182,12 @@ export class ExtractTextCommand extends ActionCommand
if (0 < this._first.value) {
result += ' :first ' + this._first;
}
return result + ' ' + this.varname + ';';
return result + ' ' + this._varname + ';';
}
pushArguments(args)
{
this.varname = args.pop();
this._varname = args.pop();
[':lower', ':upper', ':lowerfirst', ':upperfirst', ':quotewildcard', ':length'].forEach(modifier => {
args.includes(modifier) && this.modifiers.push(modifier);
});

View file

@ -14,7 +14,7 @@ export class ValidExtListTest extends TestCommand
{
constructor()
{
super();
super('valid_ext_list');
this.ext_list_names = new GrammarStringList;
}

View file

@ -16,18 +16,21 @@ export class IncludeCommand extends ControlCommand
this.global = false; // ':personal' / ':global';
this.once = false;
this.optional = false;
this.value = new GrammarQuotedString;
this._value = new GrammarQuotedString;
}
get require() { return 'include'; }
get value() { return this._value.value; }
set value(v) { this._value.value = v; }
toString()
{
return 'include'
+ (this.global ? ' :global' : '')
+ (this.once ? ' :once' : '')
+ (this.optional ? ' :optional' : '')
+ ' ' + this.value + ';';
+ ' ' + this._value + ';';
}
pushArguments(args)
@ -36,7 +39,7 @@ export class IncludeCommand extends ControlCommand
if (':global' === arg || ':once' === arg || ':optional' === arg) {
this[arg.slice(1)] = true;
} else if (arg instanceof GrammarQuotedString) {
this.value = arg;
this._value = arg;
}
});
}

View file

@ -9,7 +9,7 @@ import {
MULTILINE_DOTSTART
} from 'Sieve/RegEx';
import { arrayToString, getMatchTypes } from 'Sieve/Utils';
import { arrayToString, getMatchTypes, getComparators } from 'Sieve/Utils';
/**
* abstract
@ -18,7 +18,7 @@ export class GrammarString /*extends String*/
{
constructor(value = '')
{
this._value = value;
this._value = value.toString ? value.toString() : value;
}
toString()
@ -45,41 +45,41 @@ export class GrammarString /*extends String*/
/**
* abstract
*/
export class GrammarComment extends GrammarString
export /*abstract*/ class GrammarComment extends GrammarString
{
/*
constructor()
{
if (this.constructor == GrammarComment) {
throw Error("Abstract class can't be instantiated.");
}
}
*/
}
/**
* https://tools.ietf.org/html/rfc5228#section-2.9
*/
export class GrammarCommand
const cmdNameSuffix = /(test|command|action)$/;
export /*abstract*/ class GrammarCommand
{
constructor(identifier)
{
this.identifier = identifier || this.constructor.name.toLowerCase().replace(/(test|command|action)$/, '');
this.arguments = [];
this.commands = new GrammarCommands;
/*
if (this.constructor == GrammarCommand) {
throw Error("Abstract class can't be instantiated.");
}
*/
this.identifier = identifier || this.constructor.name.toLowerCase().replace(cmdNameSuffix, '');
}
toString()
{
let result = this.identifier;
if (this.arguments.length) {
if (this.arguments?.length) {
result += ' ' + arrayToString(this.arguments, ' ');
}
return result + (
this.commands.length ? ' ' + this.commands : ';'
);
}
getComparators()
{
return ['i;ascii-casemap'];
}
getMatchTypes()
{
return [':is', ':contains', ':matches'];
return result + ';';
}
pushArguments(args)
@ -108,78 +108,107 @@ export class GrammarCommands extends Array
/**
* https://tools.ietf.org/html/rfc5228#section-3
*/
export class ControlCommand extends GrammarCommand
export /*abstract*/ class ControlCommand extends GrammarCommand
{
/*
constructor(identifier)
{
super(identifier);
this.commands = new GrammarCommands;
}
toString()
{
let result = this.identifier;
if (this.arguments.length) {
result += ' ' + arrayToString(this.arguments, ' ');
if (this.constructor == ControlCommand) {
throw Error("Abstract class can't be instantiated.");
}
return result + (
this.commands.length ? ' ' + this.commands : ';'
);
}
getComparators()
{
return ['i;ascii-casemap'];
}
getMatchTypes()
{
return [':is', ':contains', ':matches'];
super(identifier);
}
*/
}
/**
* https://tools.ietf.org/html/rfc5228#section-4
*/
export class ActionCommand extends GrammarCommand
export /*abstract*/ class ActionCommand extends GrammarCommand
{
/*
constructor(identifier)
{
if (this.constructor == ActionCommand) {
throw Error("Abstract class can't be instantiated.");
}
super(identifier);
}
toString()
{
let result = this.identifier;
if (this.arguments.length) {
result += ' ' + arrayToString(this.arguments, ' ');
}
return result + ';'
}
*/
}
/**
* https://tools.ietf.org/html/rfc5228#section-5
*/
export class TestCommand extends GrammarCommand
export /*abstract*/ class TestCommand extends GrammarCommand
{
constructor(identifier)
{
/*
if (this.constructor == TestCommand) {
throw Error("Abstract class can't be instantiated.");
}
*/
super(identifier);
// Almost every test has a comparator and match_type, so define them here
this.comparator = '';
this.match_type = ':is';
this._comparator = '';
this._match_type = '';
this.relational_match = ''; // GrammarQuotedString DQUOTE ( "gt" / "ge" / "lt" / "le" / "eq" / "ne" ) DQUOTE
}
get require() { return /:value|:count/.test(this._match_type) ? 'relational' : ''; }
get match_type()
{
return this._match_type;
}
set match_type(value)
{
// default?
if (':is' == value) {
value = '';
}
if (value.length && !getMatchTypes(0).includes(value)) {
throw 'Unsupported match-type ' + value;
}
if (':list' == value) {
this._comparator = '';
}
if (':count' != value && ':value' != value) {
this.relational_match = '';
}
this._match_type = value;
}
get comparator()
{
return this._comparator;
}
set comparator(value)
{
if (!(value instanceof GrammarQuotedString)) {
value = new GrammarQuotedString(value);
}
// default?
if (value.length && 'i;ascii-casemap' != value.value) {
if (':list' == this._match_type) {
throw 'Comparator not allowed when using :list';
}
if (!getComparators().includes(value.value)) {
throw 'Unsupported comparator ' + value;
}
this._comparator = value;
} else {
this._comparator = '';
}
}
toString()
{
// https://datatracker.ietf.org/doc/html/rfc6134#section-2.3
if (!getMatchTypes().includes(this.match_type)) {
throw 'Unsupported match-type ' + this.match_type;
}
return (this.identifier
+ (this.comparator ? ' :comparator ' + this.comparator : '')
+ (this.match_type ? ' ' + this.match_type : '')
+ (this._comparator ? ' :comparator ' + this._comparator : '')
+ (this._match_type ? ' ' + this._match_type : '')
+ (this.relational_match ? ' ' + this.relational_match : '')
+ ' ' + arrayToString(this.arguments, ' ')).trim();
}
}
@ -251,6 +280,7 @@ export class GrammarStringList extends Array
{
toString()
{
// if there is only a single string, the brackets are optional
if (1 < this.length) {
return '[' + this.join(',') + ']';
}
@ -259,7 +289,7 @@ export class GrammarStringList extends Array
push(value)
{
if (!(value instanceof GrammarString)) {
if (!(value instanceof GrammarQuotedString)) {
value = new GrammarQuotedString(value);
}
super.push(value);
@ -286,10 +316,14 @@ GrammarStringList.fromString = list => {
export class GrammarQuotedString extends GrammarString
{
constructor(value = '')
{
super(value instanceof GrammarQuotedString ? value.value : value);
}
toString()
{
return '"' + this._value.replace(/[\\"]/g, '\\$&') + '"';
// return '"' + super.toString().replace(/[\\"]/g, '\\$&') + '"';
}
}
@ -324,3 +358,23 @@ GrammarMultiLine.fromString = string => {
}
return new GrammarMultiLine();
}
export class UnknownCommand extends GrammarCommand
{
constructor(identifier)
{
super(identifier);
this.commands = new GrammarCommands;
}
toString()
{
let result = this.identifier;
if (this.arguments?.length) {
result += ' ' + arrayToString(this.arguments, ' ');
}
return result + (
this.commands?.length ? ' ' + this.commands : ';'
);
}
}

View file

@ -18,6 +18,7 @@ import {
import {
GrammarBracketComment,
GrammarCommand,
GrammarComment,
GrammarHashComment,
GrammarMultiLine,
GrammarNumber,
@ -27,8 +28,8 @@ import {
GrammarTestList
} from 'Sieve/Grammar';
import { availableCommands } from 'Sieve/Commands';
import { ConditionalCommand, RequireCommand } from 'Sieve/Commands/Controls';
import { availableCommands, unavailableCommands } from 'Sieve/Commands';
import { ConditionalCommand, IfCommand, RequireCommand } from 'Sieve/Commands/Controls';
import { NotTest } from 'Sieve/Commands/Tests';
const
@ -92,6 +93,7 @@ export const parseScript = (script, name = 'script.sieve') => {
// Only activate available commands
const Commands = availableCommands();
const disabledCommands = unavailableCommands();
let match,
line = 1,
@ -117,18 +119,20 @@ export const parseScript = (script, name = 'script.sieve') => {
let prev_arg = args[args.length-1];
if (getMatchTypes(0).includes(arg)) {
command.match_type = arg;
} else if (':value' === prev_arg || ':count' === prev_arg) {
// Sieve relational [RFC5231] match types
/^(gt|ge|lt|le|eq|ne)$/.test(arg.value) || error('Invalid relational match-type ' + arg);
command.match_type = prev_arg + ' ' + arg;
} else if (getMatchTypes(0).includes(prev_arg)) {
--args.length;
// requires.push('relational');
if (':value' === prev_arg || ':count' === prev_arg) {
// Sieve relational [RFC5231] match types
/^"(gt|ge|lt|le|eq|ne)"$/.test(arg) || error('Invalid relational match-type ' + arg);
command.relational_match = arg;
// requires.push('relational');
return;
}
} else if (':comparator' === prev_arg) {
command.comparator = arg;
--args.length;
} else {
args.push(arg);
}
args.push(arg);
},
pushArgs = () => {
if (args.length) {
@ -154,27 +158,30 @@ export const parseScript = (script, name = 'script.sieve') => {
pushArgs();
value = value.toLowerCase();
let new_command;
if ('if' === value) {
new_command = new ConditionalCommand(value);
} else if ('elsif' === value || 'else' === value) {
// (prev_command instanceof ConditionalCommand) || error('Not after IF condition');
new_command = new ConditionalCommand(value);
} else if (Commands[value]) {
if (Commands[value]) {
if ('elsif' === value || 'else' === value) {
let valid = false, cmd = (command ? command?.commands : tree), i = cmd?.length;
while (i) {
cmd[--i];
if (cmd[i] instanceof IfCommand) {
valid = true;
break;
} else if (typeof cmd[i] !== 'string' && !(cmd[i] instanceof GrammarComment)) {
break;
}
}
valid || error('Not after IF/ELSIF condition');
}
if ('allof' === value || 'anyof' === value) {
// (command instanceof ConditionalCommand || command instanceof NotTest) || error('Test-list not in conditional');
}
new_command = new Commands[value]();
} else if (disabledCommands[value]) {
console.error('Unsupported command: ' + value);
new_command = new disabledCommands[value]();
} else {
if (command && (
command instanceof ConditionalCommand
|| command instanceof NotTest
|| command.tests instanceof GrammarTestList)) {
console.error('Unknown test: ' + value);
new_command = new TestCommand(value);
} else {
console.error('Unknown command: ' + value);
new_command = new GrammarCommand(value);
}
console.error('Unknown command: ' + value);
new_command = new GrammarCommand(value);
}
if (new_command instanceof TestCommand) {
@ -192,7 +199,7 @@ export const parseScript = (script, name = 'script.sieve') => {
if (command.commands) {
command.commands.push(new_command);
} else {
error('commands not allowed in "' + command.identifier + '" command');
error('command "' + new_command.identifier + '" not allowed in "' + command.identifier + '" command');
}
} else {
tree.push(new_command);
@ -219,7 +226,8 @@ export const parseScript = (script, name = 'script.sieve') => {
pushArg(GrammarMultiLine.fromString(value));
break;
case T_QUOTED_STRING:
pushArg(new GrammarQuotedString(value.slice(1,-1)));
try { value = JSON.parse(value); } catch(e) { console.error(e, value); }
pushArg(new GrammarQuotedString(value));
break;
case T_NUMBER:
pushArg(new GrammarNumber(value));
@ -268,7 +276,6 @@ export const parseScript = (script, name = 'script.sieve') => {
break;
case T_BLOCK_END:
(command instanceof ConditionalCommand) || error(TokenError[type]);
// prev_command = command;
command = levels.up();
break;
@ -293,5 +300,6 @@ export const parseScript = (script, name = 'script.sieve') => {
}
tree.requires = requires;
tree.toString = () => tree.join('\r\n');
return tree;
};

View file

@ -22,7 +22,7 @@ export const
koComputable = fn => ko.computed(fn, {'pure':true}),
arrayToString = (arr, separator) =>
arr.map(item => item.toString?.() || item).join(separator),
(arr || []).map(item => item.toString?.() || item).join(separator),
/*
getNotificationMessage = code => {
let key = getKeyByValue(Notifications, code);
@ -58,11 +58,33 @@ export const
serverErrorDesc(text);
},
getComparators = (validOnly = 0) => {
let result = [
// Default
'i;ascii-casemap',
];
if (capa.includes('relational') || !validOnly) {
result.push('i;octet');
}
if (capa.includes('comparator-i;ascii-numeric') || !validOnly) {
result.push('i;ascii-numeric');
}
if (capa.includes('comparator-i;unicode-casemap') || !validOnly) {
result.push('i;unicode-casemap');
}
return result;
},
getMatchTypes = (validOnly = 1) => {
let result = [':is',':contains',':matches'];
// https://datatracker.ietf.org/doc/html/rfc6134#section-2.3
// Only available for tests with a key_list property
if (capa.includes('extlists') || !validOnly) {
result.push(':list');
}
if (capa.includes('relational') || !validOnly) {
result.push(':value');
result.push(':count');
}
return result;
};

View file

@ -30,8 +30,6 @@ const
sDeepPrefix = '\u00A0\u00A0\u00A0',
showUnsubscribed = true/*!SettingsUserStore.hideUnsubscribed()*/,
disabled = rl.settings.get('sieveAllowFileintoInbox') ? '' : 'INBOX',
foldersWalk = folders => {
folders.forEach(oItem => {
if (showUnsubscribed || oItem.hasSubscriptions() || !oItem.exists) {
@ -39,7 +37,7 @@ const
id: oItem.fullName,
name: sDeepPrefix.repeat(oItem.deep) + oItem.detailedName(),
system: false,
disabled: !oItem.selectable() || disabled == oItem.fullName
disabled: !oItem.selectable()
});
}
@ -68,7 +66,7 @@ export class FilterPopupView extends rl.pluginPopupView {
});
this.defaultOptionsAfterRender = defaultOptionsAfterRender;
this.folderSelectList = koComputable(() => folderListOptionsBuilder());
this.folderSelectList = koComputable(folderListOptionsBuilder);
this.selectedFolderValue.subscribe(() => this.filter().actionValueError(false));

View file

@ -78,7 +78,7 @@ export class SieveScriptPopupView extends rl.pluginPopupView {
if (iError) {
self.saveError(true);
self.errorText(data?.ErrorMessageAdditional || getNotification(iError));
self.errorText(data?.messageAdditional || getNotification(iError));
} else {
script.exists() || scripts.push(script);
script.exists(true);

View file

@ -23,7 +23,7 @@ try {
data = data ? decodeURIComponent(data[2]) : null;
data = data ? JSON.parse(data) : {};
win[sName] = {
getItem: key => data[key] == null ? null : data[key],
getItem: key => data[key] ?? null,
setItem: (key, value) => {
data[key] = ''+value; // forces the value to a string
document.cookie = sName+'='+encodeURIComponent(JSON.stringify(data))

View file

@ -1,8 +1,155 @@
import { AskPopupView } from 'View/Popup/Ask';
import { SettingsUserStore } from 'Stores/User/Settings';
import { SettingsGet } from '../Common/Globals';
import { isArray } from '../Common/Utils';
export const Passphrases = new WeakMap();
Passphrases.ask = async (key, sAskDesc, btnText) =>
Passphrases.has(key)
? {password:Passphrases.get(key)/*, remember:false*/}
: await AskPopupView.password(sAskDesc, btnText);
Passphrases.ask = async (key, sAskDesc, btnText) => {
if (Passphrases.has(key)) {
return { password: Passphrases.handle(key)/*, remember:false*/ };
} else if (Passphrases.hasInLocalStorage(key)) {
return { password: await getFromLocalStorage(key) };
} else {
const pass = await AskPopupView.password(sAskDesc, btnText,
window.crypto.subtle && canUseLocalStorage(key) ? 0b1101 : 0b0101);
pass.rememberPermanent && await saveToLocalStorage(key, pass.password);
return pass;
}
};
Passphrases._deleteFromSession = Passphrases.delete;
const timeouts = {};
// get/set accessor to control deletion after N minutes of inactivity
Passphrases.handle = (key, pass) => {
const timeout = SettingsUserStore.keyPassForget();
if (timeout && !timeouts[key]) {
timeouts[key] = (() => Passphrases._deleteFromSession(key)).debounce(timeout * 60 * 1000);
}
pass && Passphrases.set(key, pass);
timeout && timeouts[key]();
return Passphrases.get(key);
};
const deleteFromLocalStorage = (key) => {
const keyId = getKeyId(key);
if (keyId) {
localStorage.removeItem(keyId);
}
};
Passphrases.delete = (key) => {
deleteFromLocalStorage(key);
return Passphrases._deleteFromSession(key);
};
Passphrases.hasInLocalStorage = (key) => {
const keyId = getKeyId(key);
return keyId && localStorage.getItem(keyId) !== null;
};
const saveToLocalStorage = async (key, pass) => {
const keyId = getKeyId(key);
if (!keyId) {
return;
}
if (!pass) {
localStorage.removeItem(keyId);
return;
}
try {
const salt = window.crypto.getRandomValues(new Uint8Array(16));
const derivedKey = await deriveKeyFromHash(SettingsGet('accountHash'), salt);
const iv = window.crypto.getRandomValues(new Uint8Array(12));
const encrypted = await window.crypto.subtle.encrypt(
{ name: 'AES-GCM', iv: iv },
derivedKey,
new TextEncoder().encode(pass)
);
localStorage.setItem(keyId, JSON.stringify([
btoa(String.fromCharCode.apply(null, salt)),
btoa(String.fromCharCode.apply(null, iv)),
btoa(String.fromCharCode.apply(null, new Uint8Array(encrypted)))
]));
} catch (e) {
console.error('Passphrases.saveToLocalStorage failed', e);
}
};
const getFromLocalStorage = async (key) => {
const keyId = getKeyId(key);
if (!keyId) {
return undefined;
}
const jsonData = localStorage.getItem(keyId);
if (!jsonData) {
console.error('Passphrases.getFromLocalStorage failed: no data found');
return undefined;
}
try {
const saltIvData = JSON.parse(jsonData);
if (!saltIvData || !isArray(saltIvData) || saltIvData.length !== 3) {
// noinspection ExceptionCaughtLocallyJS
throw new Error('invalid passphrase data');
}
const toUint8 = (str) => new Uint8Array(atob(str).split('').map(c => c.charCodeAt(0)));
const derivedKey = await deriveKeyFromHash(SettingsGet('accountHash'), toUint8(saltIvData[0]));
const decrypted = await window.crypto.subtle.decrypt(
{
name: 'AES-GCM',
iv: toUint8(saltIvData[1])
},
derivedKey,
toUint8(saltIvData[2])
);
return String.fromCharCode.apply(null, new Uint8Array(decrypted));
} catch (e) {
localStorage.removeItem(keyId);
console.error('Passphrases.getFromLocalStorage failed', e);
return undefined;
}
};
const canUseLocalStorage = (key) => getKeyId(key) !== undefined;
const getKeyId = (key) => {
if (key && typeof key.id === 'string' && key.id.length > 4 && typeof key.forgetPass === 'function') {
// only deal with keys that we can forget (OpenPGB, GnuPG)
return key.id + '_local_key';
} else {
console.info('Passphrases.getKeyId: unsupported key type');
return undefined;
}
};
const deriveKeyFromHash = async (hash, salt) => {
if (!hash) {
throw new Error('empty accountHash');
}
return window.crypto.subtle.importKey(
'raw',
new TextEncoder().encode(hash),
{ 'name': 'PBKDF2' },
false,
['deriveKey']
).then(keyMaterial => {
return window.crypto.subtle.deriveKey(
{
'name': 'PBKDF2',
'salt': salt,
'iterations': 512,
'hash': 'SHA-256'
},
keyMaterial,
{ 'name': 'AES-GCM', 'length': 256 },
false,
['encrypt', 'decrypt']
);
});
};

View file

@ -2,8 +2,6 @@ import { addObservablesTo, koArrayWithDestroy } from 'External/ko';
export const AccountUserStore = koArrayWithDestroy();
AccountUserStore.getEmailAddresses = () => AccountUserStore.map(item => item.email);
addObservablesTo(AccountUserStore, {
email: '',
loading: false

View file

@ -39,7 +39,7 @@ ContactUserStore.sync = fResultFunc => {
line = JSON.parse(line);
if ('ContactsSync' === line.Action) {
ContactUserStore.syncing(false);
fResultFunc?.(line.ErrorCode, line);
fResultFunc?.(line.code, line);
}
} catch (e) {
ContactUserStore.syncing(false);

View file

@ -12,6 +12,8 @@ import { OpenPgpKeyPopupView } from 'View/Popup/OpenPgpKey';
import { Passphrases } from 'Storage/Passphrases';
import { baseCollator } from 'Common/Translator';
const
findGnuPGKey = (keys, query/*, sign*/) =>
keys.find(key =>
@ -49,13 +51,20 @@ export const GnuPGUserStore = new class {
key.for = email => aEmails.includes(IDN.toASCII(email));
key.askDelete = ko.observable(false);
key.openForDeletion = ko.observable(null).askDeleteHelper();
key.askForgetPass = ko.observable(false);
key.openForPassForget = ko.observable(null).askDeleteHelper('askForgetPass');
key.forgetPass = () => {
Passphrases.delete(key);
key.hasStoredPass(false);
};
key.hasStoredPass = ko.observable(Passphrases.hasInLocalStorage(key));
key.remove = () => {
if (key.askDelete()) {
Remote.request('GnupgDeleteKey',
(iError, oData) => {
if (oData) {
if (iError) {
alert(oData.ErrorMessage);
alert(oData.message);
} else if (oData.Result) {
isPrivate
? this.privateKeys.remove(key)
@ -67,6 +76,7 @@ export const GnuPGUserStore = new class {
isPrivate: isPrivate
}
);
isPrivate && key.forgetPass();
}
};
if (isPrivate) {
@ -75,7 +85,7 @@ export const GnuPGUserStore = new class {
'GnuPG key<br>' + key.id + ' ' + key.emails[0],
btnTxt
);
pass && pass.remember && Passphrases.set(key, pass.password);
pass && pass.remember && Passphrases.handle(key, pass.password);
return pass?.password;
};
}
@ -106,7 +116,7 @@ export const GnuPGUserStore = new class {
key.view = () => key.fetch(() => showScreenPopup(OpenPgpKeyPopupView, [key]));
return key;
},
collator = new Intl.Collator(undefined, {sensitivity: 'base'}),
collator = baseCollator(),
sort = keys => keys.sort(
(a, b) => collator.compare(a.emails[0], b.emails[0]) || collator.compare(a.id, b.id)
);
@ -163,8 +173,8 @@ export const GnuPGUserStore = new class {
return fingerprints;
}
getPrivateKeyFor(query, sign) {
return findGnuPGKey(this.privateKeys, query, sign);
getPrivateKeyFor(query/*, sign*/) {
return findGnuPGKey(this.privateKeys, query/*, sign*/);
}
async decrypt(message) {

View file

@ -1,5 +1,12 @@
import { koArrayWithDestroy } from 'External/ko';
import { koArrayWithDestroy, koComputable } from 'External/ko';
import { isArray } from 'Common/Utils';
export const IdentityUserStore = koArrayWithDestroy();
IdentityUserStore.loading = ko.observable(false).extend({ debounce: 100 });
/** Returns main (login) identity */
IdentityUserStore.main = koComputable(() => {
const list = IdentityUserStore();
return isArray(list) ? list.find(item => item && !item.id()) : null;
});

View file

@ -32,8 +32,11 @@ import { SettingsGet } from 'Common/Globals';
import { SUB_QUERY_PREFIX } from 'Common/Links';
import { AppUserStore } from 'Stores/User/App';
import { baseCollator } from 'Common/Translator';
const
isChecked = item => item.checked(),
isDeleted = item => item.isDeleted(),
replaceHash = hash => {
rl.route.off();
hasher.replaceHash(hash);
@ -105,9 +108,8 @@ addComputablesTo(MessagelistUserStore, {
listCheckedOrSelected: () => {
const
selectedMessage = MessagelistUserStore.selectedMessage(),
focusedMessage = MessagelistUserStore.focusedMessage(),
checked = MessagelistUserStore.filter(item => isChecked(item));
return checked.length ? checked : (selectedMessage || focusedMessage ? [selectedMessage || focusedMessage] : []);
return checked.length ? checked : (selectedMessage ? [selectedMessage] : []);
},
listCheckedOrSelectedUidsWithSubMails: () => {
@ -135,11 +137,18 @@ MessagelistUserStore.hasChecked = koComputable(
MessagelistUserStore.hasCheckedOrSelected = koComputable(() =>
!!MessagelistUserStore.selectedMessage()
| !!MessagelistUserStore.focusedMessage()
// Issue: not all are observed?
| !!MessagelistUserStore.find(isChecked)
).extend({ rateLimit: 50 });
MessagelistUserStore.hasCheckedOrSelectedAndDeleted = koComputable(
() => !!MessagelistUserStore.listCheckedOrSelected().find(isDeleted)
).extend({ rateLimit: 50 });
MessagelistUserStore.hasCheckedOrSelectedAndUndeleted = koComputable(
() => !!MessagelistUserStore.listCheckedOrSelected().find(item => !item?.isDeleted())
).extend({ rateLimit: 50 });
MessagelistUserStore.notifyNewMessages = (folder, newMessages) => {
if (getFolderInboxName() === folder && arrayLength(newMessages)) {
@ -254,17 +263,14 @@ MessagelistUserStore.reload = (bDropPagePosition = false, bDropCurrentFolderCach
let flags = folderInfo.permanentFlags || [];
if (flags.includes('\\*')) {
/** Add Thunderbird labels */
let i = 6;
while (--i) {
flags.includes('$label'+i) || flags.push('$label'+i);
}
/** TODO: add others by default? */
}
flags.sort((a, b) => {
a = a.toUpperCase();
b = b.toUpperCase();
return (a < b) ? -1 : ((a > b) ? 1 : 0);
});
folder.permanentFlags(flags);
folder.permanentFlags(flags.sort(baseCollator().compare));
MessagelistUserStore.notifyNewMessages(folder.fullName, collection.newMessages);
}
@ -314,6 +320,7 @@ MessagelistUserStore.reload = (bDropPagePosition = false, bDropCurrentFolderCach
if (AppUserStore.threadsAllowed() && SettingsUserStore.useThreads()) {
params.useThreads = 1;
params.threadAlgorithm = SettingsUserStore.threadAlgorithm();
params.threadUid = MessagelistUserStore.threadUid();
} else {
params.threadUid = 0;
@ -370,6 +377,14 @@ MessagelistUserStore.setAction = (sFolderFullName, iSetAction, messages) => {
messages.forEach(oMessage =>
oMessage.isFlagged() && rootUids.push(oMessage.uid) && oMessage.flags.remove('\\flagged')
);
} else if (iSetAction == MessageSetAction.SetDeleted) {
messages.forEach(oMessage =>
!oMessage.isDeleted() && rootUids.push(oMessage.uid) && oMessage.flags.push('\\deleted')
);
} else if (iSetAction == MessageSetAction.UnsetDeleted) {
messages.forEach(oMessage =>
oMessage.isDeleted() && rootUids.push(oMessage.uid) && oMessage.flags.remove('\\deleted')
);
}
rootUids = rootUids.validUnique();
length = rootUids.length;
@ -399,6 +414,15 @@ MessagelistUserStore.setAction = (sFolderFullName, iSetAction, messages) => {
setAction: iSetAction == MessageSetAction.SetFlag ? 1 : 0
});
break;
case MessageSetAction.SetDeleted:
case MessageSetAction.UnsetDeleted:
Remote.request('MessageSetDeleted', null, {
folder: sFolderFullName,
uids: rootUids.join(','),
setAction: iSetAction == MessageSetAction.SetDeleted ? 1 : 0
});
break;
// no default
}
}
@ -444,23 +468,6 @@ MessagelistUserStore.moveMessages = (
if (page > MessagelistUserStore.pageCount()) {
setPage = MessagelistUserStore.pageCount();
}
if (MessagelistUserStore.threadUid()
&& MessagelistUserStore.length
&& MessagelistUserStore.find(item => item?.deleted() && item.uid == MessagelistUserStore.threadUid())
) {
const message = MessagelistUserStore.find(item => item && !item.deleted());
if (!message) {
if (1 < page) {
setPage = page - 1;
} else {
MessagelistUserStore.threadUid(0);
setPage = MessagelistUserStore.pageBeforeThread();
}
} else if (MessagelistUserStore.threadUid() != message.uid) {
MessagelistUserStore.threadUid(message.uid);
setPage = page;
}
}
if (setPage) {
MessagelistUserStore.page(setPage);
replaceHash(
@ -518,7 +525,6 @@ MessagelistUserStore.moveMessages = (
currentMessage = null;
MessageUserStore.message(null);
}
item.deleted(true);
MessagelistUserStore.remove(item);
});
}

View file

@ -13,7 +13,11 @@ import { OpenPgpKeyPopupView } from 'View/Popup/OpenPgpKey';
import { Passphrases } from 'Storage/Passphrases';
import { baseCollator, getNotification } from 'Common/Translator';
const
loaded = () => !!window.openpgp,
findOpenPGPKey = (keys, query/*, sign*/) =>
keys.find(key =>
key.for(query) || query == key.id || query == key.fingerprint
@ -34,12 +38,12 @@ const
privateKey: privateKey.key,
passphrase
});
result && pass.remember && Passphrases.set(privateKey, passphrase);
result && pass.remember && Passphrases.handle(privateKey, passphrase);
return result;
}
},
collator = new Intl.Collator(undefined, {sensitivity: 'base'}),
collator = baseCollator(),
sort = keys => keys.sort(
// (a, b) => collator.compare(a.emails[0], b.emails[0]) || collator.compare(a.fingerprint, b.fingerprint)
(a, b) => collator.compare(a.emails[0], b.emails[0]) || collator.compare(a.id, b.id)
@ -54,10 +58,9 @@ const
*/
publicKeysItem = 'openpgp-public-keys',
privateKeysItem = 'openpgp-private-keys',
storage = window.localStorage,
loadOpenPgpKeys = async itemname => {
let keys = [], key,
armoredKeys = JSON.parse(storage.getItem(itemname)),
armoredKeys = JSON.parse(localStorage.getItem(itemname)),
i = arrayLength(armoredKeys);
while (i--) try {
key = await openpgp.readKey({armoredKey:armoredKeys[i]});
@ -70,9 +73,9 @@ const
storeOpenPgpKeys = (keys, section) => {
let armoredKeys = keys.map(item => item.armor);
if (armoredKeys.length) {
storage.setItem(section, JSON.stringify(armoredKeys));
localStorage.setItem(section, JSON.stringify(armoredKeys));
} else {
storage.removeItem(section);
localStorage.removeItem(section);
}
};
@ -87,6 +90,9 @@ class OpenPgpKeyModel {
this.armor = armor;
this.askDelete = ko.observable(false);
this.openForDeletion = ko.observable(null).askDeleteHelper();
this.hasStoredPass = ko.observable(Passphrases.hasInLocalStorage(this));
this.askForgetPass = ko.observable(false);
this.openForPassForget = ko.observable(null).askDeleteHelper('askForgetPass');
// key.getUserIDs()
// key.getPrimaryUser()
}
@ -112,12 +118,29 @@ class OpenPgpKeyModel {
if (this.key.isPrivate()) {
OpenPGPUserStore.privateKeys.remove(this);
storeOpenPgpKeys(OpenPGPUserStore.privateKeys, privateKeysItem);
this.forgetPass()
} else {
OpenPGPUserStore.publicKeys.remove(this);
storeOpenPgpKeys(OpenPGPUserStore.publicKeys, publicKeysItem);
}
Remote.request('DeletePGPKey',
(iError, oData) => {
if (oData) {
if (iError || oData.Result === false) {
alert(oData.message || getNotification(iError));
}
}
}, {
key: this.armor
}
);
}
}
forgetPass() {
Passphrases.delete(this);
this.hasStoredPass(false);
}
/*
toJSON() {
return this.armor;
@ -132,23 +155,36 @@ export const OpenPGPUserStore = new class {
}
loadKeyrings() {
if (window.openpgp) {
loadOpenPgpKeys(publicKeysItem).then(keys => {
if (loaded()) {
loadOpenPgpKeys(publicKeysItem)
.then(keys => {
this.publicKeys(dedup(keys));
console.log('openpgp.js public keys loaded');
});
loadOpenPgpKeys(privateKeysItem).then(keys => {
this.privateKeys(dedup(keys));
console.log('openpgp.js private keys loaded');
})
.finally(() => {
loadOpenPgpKeys(privateKeysItem)
.then(keys => {
this.privateKeys(dedup(keys));
console.log('openpgp.js private keys loaded');
})
.finally(() => {
/*SettingsGet('loadBackupKeys') && */this.loadBackupKeys();
});
});
}
}
loadBackupKeys() {
Remote.request('GetPGPKeys',
(iError, oData) => !iError && oData.Result && this.importKeys(oData.Result)
);
}
/**
* @returns {boolean}
*/
isSupported() {
return !!window.openpgp;
return loaded();
}
importKey(armoredKey) {
@ -156,7 +192,7 @@ export const OpenPGPUserStore = new class {
}
async importKeys(keys) {
if (window.openpgp) {
if (loaded()) {
const privateKeys = this.privateKeys(),
publicKeys = this.publicKeys();
for (const armoredKey of keys) try {
@ -183,7 +219,7 @@ export const OpenPGPUserStore = new class {
keyPair.revocationCertificate
*/
storeKeyPair(keyPair) {
if (window.openpgp) {
if (loaded()) {
openpgp.readKey({armoredKey:keyPair.publicKey}).then(key => {
this.publicKeys.push(new OpenPgpKeyModel(keyPair.publicKey, key));
storeOpenPgpKeys(this.publicKeys, publicKeysItem);

View file

@ -61,7 +61,7 @@ export const
if (gnuPG && oData?.Result/* && (oData.Result.imported || oData.Result.secretimported)*/) {
GnuPGUserStore.loadKeyrings();
}
iError && alert(oData.ErrorMessage);
iError && alert(oData.message);
}, {
key, gnuPG, backup
}

View file

@ -1,4 +1,5 @@
import { addObservablesTo, koArrayWithDestroy } from 'External/ko';
import { baseCollator } from 'Common/Translator';
import Remote from 'Remote/User/Fetch';
export const SMimeUserStore = koArrayWithDestroy();
@ -12,7 +13,7 @@ SMimeUserStore.loadCertificates = () => {
SMimeUserStore.loading(true);
Remote.request('SMimeGetCertificates', (iError, oData) => {
SMimeUserStore.loading(false);
const collator = new Intl.Collator(undefined, {sensitivity: 'base'});
const collator = baseCollator();
iError || SMimeUserStore(oData.Result.sort(
(a, b) => collator.compare(a.emailAddress, b.emailAddress) || (b.validTo_time_t - a.validTo_time_t)
));

View file

@ -5,6 +5,7 @@ import { LayoutSideView, LayoutBottomView } from 'Common/EnumsUser';
import { pInt } from 'Common/Utils';
import { $htmlCL, SettingsGet, fireEvent } from 'Common/Globals';
import { ThemeStore } from 'Stores/Theme';
import { FolderUserStore } from 'Stores/User/Folder';
export const SettingsUserStore = new class {
constructor() {
@ -22,6 +23,7 @@ export const SettingsUserStore = new class {
allowStyles: 0,
collapseBlockquotes: 1,
maxBlockquotesLevel: 0,
minFontSize: 0,
listInlineAttachments: 0,
simpleAttachmentsList: 0,
useCheckboxesInList: 1,
@ -29,11 +31,14 @@ export const SettingsUserStore = new class {
showNextMessage: 0,
allowDraftAutosave: 1,
useThreads: 0,
defaultSort: '',
threadAlgorithm: '',
replySameFolder: 0,
hideUnsubscribed: 0,
hideDeleted: 1,
unhideKolabFolders: 0,
autoLogout: 0,
keyPassForget: 15,
showUnreadCount: 0,
messageNewWindow: 0,
messageReadAuto: 0,
@ -48,6 +53,7 @@ export const SettingsUserStore = new class {
layout: 1,
editorDefaultType: 'Html',
editorWysiwyg: 'Squire',
markdown: 0,
msgDefaultAction: 1
});
@ -93,13 +99,16 @@ export const SettingsUserStore = new class {
'AllowStyles',
'CollapseBlockquotes',
'MaxBlockquotesLevel',
'minFontSize',
'ListInlineAttachments',
'simpleAttachmentsList',
'UseCheckboxesInList',
'listGrouped',
'showNextMessage',
'AllowDraftAutosave',
'UseThreads',
'useThreads',
'defaultSort',
'threadAlgorithm',
'ReplySameFolder',
'HideUnsubscribed',
'HideDeleted',
@ -110,7 +119,41 @@ export const SettingsUserStore = new class {
'requireTLS',
'pgpSign',
'pgpEncrypt',
'allowSpellcheck'
'allowSpellcheck',
'markdown'
/*
'MessagesPerPage',
'MessageReadDelay',
'SoundNotification',
'NotificationSound',
'DesktopNotifications',
'Layout',
'AutoLogout',
'ContactsAutosave',
'contactsAllowed',
'CheckMailInterval',
'SentFolder',
'DraftsFolder',
'JunkFolder',
'TrashFolder',
'ArchiveFolder',
'hourCycle',
'Resizer4Width',
'Resizer5Width',
'Resizer5Height',
'fontSansSerif',
'fontSerif',
'fontMono',
'userBackgroundName',
'userBackgroundHash',
'autoVerifySignatures',
'allowLanguagesOnSettings',
'attachmentLimit',
'Theme',
'language',
'clientLanguage',
'StaticLibsJs',
*/
].forEach(name => {
let value = SettingsGet(name);
name = name[0].toLowerCase() + name.slice(1);
@ -122,5 +165,8 @@ export const SettingsUserStore = new class {
self.checkMailInterval(pInt(SettingsGet('CheckMailInterval')));
self.messageReadDelay(pInt(SettingsGet('MessageReadDelay')));
self.autoLogout(pInt(SettingsGet('AutoLogout')));
self.keyPassForget(pInt(SettingsGet('keyPassForget')));
FolderUserStore.sortMode(self.defaultSort());
}
};

View file

@ -34,4 +34,9 @@
display: block;
}
.tab-content {
grid-column-end: 3;
border: 1px solid var(--border-color, #ddd);
padding: 10px;
}
}

View file

@ -1,6 +1,6 @@
#V-PopupsLanguages {
max-width: 710px;
max-width: 720px;
label {
display: inline-block;

View file

@ -9,7 +9,8 @@
#V-PopupsCompose {
height: 100vh;
height: 100dvh;
max-width: 1000px;
max-width: min(99vw, 1600px);
resize: horizontal;
width: 99%;
.modal-body {
@ -21,12 +22,18 @@
.tabs {
height: 100%;
max-height: 100%;
overflow: auto;
}
.tabs > label > * + * {
margin-left: 0.5em;
}
.tabs .tab-content {
overflow: auto;
}
.textAreaParent, .attachmentAreaParent {
height: 100%;
min-height: 200px;

View file

@ -125,8 +125,9 @@
/* Right */
.right {
width: 100%;
border-left: 1px solid var(--border-color, #ddd);
max-width: 600px;
width: 100%;
}
.b-view-content-toolbar {

View file

@ -82,7 +82,7 @@
border-left: 3px solid transparent;
color: var(--folders-color, #333);
display: block;
line-height: 34px;
line-height: 2.2em;
padding: 0 2em 0 @folderItemPadding;
position: relative;
text-decoration: none;
@ -151,7 +151,7 @@
color: var(--unread-count-color, #fff);
font-size: 80%;
line-height: 1.5em;
margin-top: 7px;
margin-top: 0.5em;
min-width: 1.7em;
padding: 1px 4px;
text-align: center;

View file

@ -207,10 +207,6 @@
font-weight: bolder;
}
&.deleted {
opacity: .3;
}
.messageCheckbox {
font-size: 114%;
padding: 5px .5em 0;
@ -327,6 +323,7 @@
.attachmentParent,
.flagParent {
margin: 0 10px 0 5px;
min-width: 1em;
}
.flagParent::after {
content: '☆'; /*⚐*/

View file

@ -45,4 +45,8 @@
content: ')';
}
.identities-list tr + tr .identity-default {
display: none;
}
}

View file

@ -56,6 +56,12 @@
.squire-wysiwyg {
font-family: var(--fontSans);
/* Prevent collapse of empty elements so that the caret can be placed in there
*:empty::before {
content: "\200B";
}
*/
ul {
padding-left: 40px;
li {

View file

@ -16,7 +16,7 @@
text-align: var(--left, left);
top: 100%;
transition: opacity 0.2s ease-in-out;
white-space: nowrap;
white-space: pre;
z-index: 2001;
}
@media screen and (max-width: @maxMobileWidth) {
@ -33,6 +33,11 @@ input:-webkit-autofill:active {
transition: color 9999s ease-out, background-color 9999s ease-out;
}
.btn-rotate,
[data-icon].rotate::before {
transform: rotate(-180deg);
}
/*
@media (pointer: coarse), (hover: none) {
[title] {

View file

@ -36,7 +36,7 @@ export class AccountPopupView extends AbstractViewPopup {
this.submitRequest(false);
if (iError) {
this.submitError(getNotification(iError));
this.submitErrorAdditional(data?.ErrorMessageAdditional);
this.submitErrorAdditional(data?.messageAdditional);
} else {
loadAccountsAndIdentities();
this.close();

View file

@ -18,7 +18,7 @@ export class AdvancedSearchPopupView extends AbstractViewPopup {
text: '',
keyword: '',
repliedValue: -1,
selectedDateValue: -1,
selectedDateValue: 0,
selectedTreeValue: '',
hasAttachment: false,
@ -32,14 +32,12 @@ export class AdvancedSearchPopupView extends AbstractViewPopup {
// Almost the same as MessageModel.tagOptions
keywords: () => {
const keywords = [{value:'',label:''}];
FolderUserStore.currentFolder().permanentFlags.forEach(value => {
if (isAllowedKeyword(value)) {
let lower = value.toLowerCase();
keywords.push({
value: value,
label: i18n('MESSAGE_TAGS/'+lower, 0, lower)
});
}
FolderUserStore.currentFolder().optionalTags().forEach(value => {
let lower = value.toLowerCase();
keywords.push({
value: value,
label: i18n('MESSAGE_TAGS/'+lower, 0, lower)
});
});
return keywords
},
@ -57,15 +55,25 @@ export class AdvancedSearchPopupView extends AbstractViewPopup {
selectedDates: () => {
translateTrigger();
let prefix = 'SEARCH/DATE_';
// We should think about migrating all SEARCH/DATE_ to either SEARCH/SINCE_DATE or SEARCH/BEFORE_DATE
// and adjust the prefix accordingly
// let prefix_since = 'SEARCH/SINCE_DATE_';
let prefix = 'SEARCH/SINCE_';
let prefix_before = 'SEARCH/BEFORE_';
return [
{ id: -1, name: i18n(prefix + 'ALL') },
{ id: 0, name: i18n('SEARCH/DATE_ALL') },
{ id: 3, name: i18n(prefix + '3_DAYS') },
{ id: 7, name: i18n(prefix + '7_DAYS') },
{ id: 30, name: i18n(prefix + 'MONTH') },
{ id: 90, name: i18n(prefix + '3_MONTHS') },
{ id: 180, name: i18n(prefix + '6_MONTHS') },
{ id: 365, name: i18n(prefix + 'YEAR') }
{ id: 365, name: i18n(prefix + 'YEAR') },
{ id: -3, name: i18n(prefix_before + '3_DAYS') },
{ id: -7, name: i18n(prefix_before + '7_DAYS') },
{ id: -30, name: i18n(prefix_before + 'MONTH') },
{ id: -90, name: i18n(prefix_before + '3_MONTHS') },
{ id: -180, name: i18n(prefix_before + '6_MONTHS') },
{ id: -365, name: i18n(prefix_before + 'YEAR') }
];
},
@ -102,11 +110,16 @@ export class AdvancedSearchPopupView extends AbstractViewPopup {
append('text', self.text().trim());
append('keyword', self.keyword());
append('in', self.selectedTreeValue());
if (-1 < self.selectedDateValue()) {
if (0 < self.selectedDateValue()) {
let d = new Date();
d.setDate(d.getDate() - self.selectedDateValue());
append('since', d.toISOString().split('T')[0]);
}
else if (-1 > self.selectedDateValue()) {
let d = new Date();
d.setDate(d.getDate() + self.selectedDateValue());
append('before', d.toISOString().split('T')[0]);
}
let result = decodeURIComponent(new URLSearchParams(data).toString());
@ -138,8 +151,7 @@ export class AdvancedSearchPopupView extends AbstractViewPopup {
self.text(pString(params.get('text')));
self.keyword(pString(params.get('keyword')));
self.selectedTreeValue(pString(params.get('in')));
// self.selectedDateValue(params.get('since'));
self.selectedDateValue(-1);
self.selectedDateValue(0);
self.hasAttachment(params.has('attachment'));
self.starred(params.has('flagged'));
self.unseen(params.has('unseen'));

View file

@ -15,10 +15,14 @@ export class AskPopupView extends AbstractViewPopup {
askUsername: false,
passphrase: '',
askPass: false,
remember: true,
askRemeber: false
remember: true, // remember for session
askRemeber: false,
rememberPermanent: false,
askRememberPermanent: false,
});
this.rememberPermanent.subscribe(value => value && this.remember(true));
this.fYesAction = null;
this.fNoAction = null;
@ -48,10 +52,12 @@ export class AskPopupView extends AbstractViewPopup {
this.askDesc(sAskDesc || '');
this.askUsername(ask & 2);
this.askPass(ask & 1);
this.askRemeber(ask & 4);
this.askRemeber(ask & 4); // 0b0100
this.askRememberPermanent(ask & 0b1000)
this.username('');
this.passphrase('');
this.remember(true);
this.rememberPermanent(false);
this.yesButton(i18n(btnText || 'GLOBAL/YES'));
this.noButton(i18n(ask ? 'GLOBAL/CANCEL' : 'GLOBAL/NO'));
this.fYesAction = fYesFunc;

View file

@ -44,6 +44,7 @@ import Remote from 'Remote/User/Fetch';
import { ComposeAttachmentModel } from 'Model/ComposeAttachment';
import { EmailModel } from 'Model/Email';
import { IdentityModel } from 'Model/Identity';
import { MimeHeaderAutocryptModel } from 'Model/MimeHeaderAutocrypt';
import { addressparser } from 'Mime/Address';
@ -92,7 +93,7 @@ const
findIdentity = addresses => {
addresses = addresses.map(item => item.email);
return IdentityUserStore.find(item => addresses.includes(item.email()));
return IdentityUserStore.find(item => addresses.includes(item.email));
},
/**
@ -298,7 +299,7 @@ export class ComposePopupView extends AbstractViewPopup {
]
});
this.tryToClose = this.tryToClose.debounce(200);
this.doClose = this.doClose.debounce(200);
this.iTimer = 0;
@ -336,11 +337,14 @@ export class ComposePopupView extends AbstractViewPopup {
canMailvelope: () => this.encryptOptions.includes('Mailvelope'),
canSign: () => this.signOptions().length,
encryptOptionsText: () => this.encryptOptions().join(', '),
signOptionsText: () => this.signOptions().map(o => o[0]).join(', '),
identitiesOptions: () =>
IdentityUserStore.map(item => ({
item: item,
optValue: item.id(),
optText: item.formattedName()
optText: item
})),
canBeSentOrSaved: () => !this.sending() && !this.saving()
@ -355,7 +359,7 @@ export class ComposePopupView extends AbstractViewPopup {
currentIdentity: value => {
if (value) {
this.from(value.formattedName());
this.from(value.toString());
this.doEncrypt(value.pgpEncrypt() || SettingsUserStore.pgpEncrypt());
this.doSign(value.pgpSign() || SettingsUserStore.pgpSign());
}
@ -451,13 +455,24 @@ export class ComposePopupView extends AbstractViewPopup {
contactsCommand: self => self.allowContacts
});
this.from(IdentityUserStore()[0].formattedName());
this.from(IdentityUserStore()[0].toString());
}
sentFolder()
{
let sSentFolder = this.currentIdentity()?.sentFolder?.() || FolderUserStore.sentFolder();
if (SettingsUserStore.replySameFolder()) {
if (
3 === arrayLength(this.aDraftInfo) &&
this.aDraftInfo[2]?.length
) {
sSentFolder = this.aDraftInfo[2];
}
}
return UNUSED_OPTION_VALUE === sSentFolder ? null : sSentFolder;
}
sendCommand() {
const identity = this.currentIdentity();
let sSentFolder = identity?.sentFolder?.() || FolderUserStore.sentFolder();
this.attachmentsInProcessError(false);
this.attachmentsInErrorError(false);
this.emptyToError(false);
@ -475,80 +490,95 @@ export class ComposePopupView extends AbstractViewPopup {
}
if (!this.emptyToError() && !this.attachmentsInErrorError() && !this.attachmentsInProcessError()) {
if (SettingsUserStore.replySameFolder()) {
if (
3 === arrayLength(this.aDraftInfo) &&
null != this.aDraftInfo[2] &&
this.aDraftInfo[2].length
) {
sSentFolder = this.aDraftInfo[2];
}
}
if (!sSentFolder) {
const sSentFolder = this.sentFolder();
if ('' === sSentFolder) {
showScreenPopup(FolderSystemPopupView, [FolderType.Sent]);
} else try {
this.sendError(false);
this.sending(true);
sSentFolder = UNUSED_OPTION_VALUE === sSentFolder ? '' : sSentFolder;
this.getMessageRequestParams(sSentFolder).then(params => {
Remote.request('SendMessage',
(iError, data) => {
this.sending(false);
if (iError) {
if (Notifications.CantSaveMessage === iError) {
this.sendSuccessButSaveError(true);
let msg = i18n('COMPOSE/SAVED_ERROR_ON_SEND');
if (data?.ErrorMessageAdditional) {
msg = msg + "\n" + data?.ErrorMessageAdditional;
}
this.savedErrorDesc(msg);
} else {
params.signPassphrase && Passphrases.delete(identity);
this.sendError(true);
this.sendErrorDesc(getNotification(iError, data?.ErrorMessage)
|| getNotification(Notifications.CantSendMessage));
}
} else {
if (arrayLength(this.aDraftInfo) > 0) {
const flag = {
'reply': '\\answered',
'forward': '$forwarded'
}[this.aDraftInfo[0]];
if (flag) {
const aFlags = oLastMessage.flags();
if (aFlags.indexOf(flag) === -1) {
aFlags.push(flag);
oLastMessage.flags(aFlags);
}
}
}
this.close();
}
setFolderETag(this.draftsFolder(), '');
setFolderETag(sSentFolder, '');
if (3 === arrayLength(this.aDraftInfo)) {
const folder = this.aDraftInfo[2];
setFolderETag(folder, '');
}
reloadDraftFolder();
},
params,
30000
);
}).catch(e => {
} else {
const sendError = e => {
console.error(e);
this.sendError(true);
this.sendErrorDesc(e);
this.sending(false);
});
} catch (e) {
console.error(e);
this.sendError(true);
this.sendErrorDesc(e);
this.sending(false);
};
const sendFailed = (iError, data) => {
this.sendError(true);
this.sendErrorDesc(
getNotification(iError, data?.message, Notifications.CantSendMessage)
+ "\n" + (data?.messageAdditional || data?.message)
);
};
try {
this.sendError(false);
this.sending(true);
const sendMessage = params => {
Remote.request('SendMessage',
(iError, data) => {
this.sending(false);
if (iError) {
/*
if (Notifications.AuthError === iError && !params.auth) {
AskPopupView.password('SMTP login', 'retry', 3).then(result => {
if (result) {
this.sending(true);
params.auth = result;
sendMessage(params);
} else {
sendFailed(iError, data);
}
});
} else
*/
if (Notifications.CantSaveMessage === iError) {
this.sendSuccessButSaveError(true);
let msg = i18n('COMPOSE/SAVED_ERROR_ON_SEND');
if (data?.messageAdditional) {
msg = msg + "\n" + data?.messageAdditional;
}
this.savedErrorDesc(msg);
} else {
this.sendError(true);
sendFailed(iError, data);
// Remove remembered passphrase as it could be wrong
let key = ('S/MIME' === params.sign) ? this.currentIdentity() : null;
params.signFingerprint
&& this.signOptions.forEach(option => ('GnuPG' === option[0]) && (key = option[1]));
key && Passphrases.delete(key);
}
} else {
if (arrayLength(this.aDraftInfo) > 0) {
const flag = {
'reply': '\\answered',
'forward': '$forwarded'
}[this.aDraftInfo[0]];
if (flag) {
const aFlags = oLastMessage.flags();
if (aFlags.indexOf(flag) === -1) {
aFlags.push(flag);
oLastMessage.flags(aFlags);
}
}
}
this.close();
}
setFolderETag(this.draftsFolder(), '');
setFolderETag(params.saveFolder, '');
if (3 === arrayLength(this.aDraftInfo)) {
setFolderETag(this.aDraftInfo[2], '');
}
reloadDraftFolder();
},
params,
30000
);
};
this.getMessageRequestParams(sSentFolder)
.then(sendMessage)
.catch(sendError);
} catch (e) {
sendError(e);
}
}
}
}
@ -636,7 +666,7 @@ export class ComposePopupView extends AbstractViewPopup {
this.saveCommand();
}
this.tryToClose();
this.doClose();
}
contactsCommand() {
@ -833,6 +863,7 @@ export class ComposePopupView extends AbstractViewPopup {
case ComposeType.Forward:
case ComposeType.ForwardAsAttachment:
identity = findIdentity(oLastMessage.to.concat(oLastMessage.cc, oLastMessage.bcc))
|| findIdentity(oLastMessage.from)
/* || findIdentity(oLastMessage.deliveredTo)*/;
break;
case ComposeType.Draft:
@ -842,10 +873,20 @@ export class ComposePopupView extends AbstractViewPopup {
// case ComposeType.Empty:
}
}
// Set from custom email
if (!identity
&& oLastMessage && (ComposeType.Reply === options.mode || ComposeType.ReplyAll === options.mode)
&& 1 === oLastMessage.to.length
// && mEmail.includes(oLastMessage.to[0].domain)
) {
identity = new IdentityModel;
identity.name = oLastMessage.to[0].name;
identity.email = oLastMessage.to[0].email;
}
identity = identity || IdentityUserStore()[0];
if (identity) {
// excludeEmail.add(identity.email());
excludeEmail[identity.email()] = true;
// excludeEmail.add(identity.email);
excludeEmail[identity.email] = true;
}
if (arrayLength(options.to)) {
@ -861,7 +902,7 @@ export class ComposePopupView extends AbstractViewPopup {
}
if (options.mode && oLastMessage) {
let encrypted,
let usePlain,
sCc = '',
sDate = timestampToString(oLastMessage.dateTimestamp(), 'FULL'),
sSubject = oLastMessage.subject(),
@ -889,10 +930,9 @@ export class ComposePopupView extends AbstractViewPopup {
oLastMessage.headers().valuesByName('autocrypt').forEach(value => {
let autocrypt = new MimeHeaderAutocryptModel(value);
if (autocrypt.addr && autocrypt.keydata) {
PgpUserStore.hasPublicKeyForEmails([autocrypt.addr]).then(result =>
result || PgpUserStore.importKey(autocrypt.pem(), true, true)
// result || showScreenPopup(OpenPgpImportPopupView, [autocrypt.pem()])
);
PgpUserStore.hasPublicKeyForEmails([autocrypt.addr])
|| PgpUserStore.importKey(autocrypt.pem(), true, true)
// || showScreenPopup(OpenPgpImportPopupView, [autocrypt.pem()])
}
});
} break;
@ -931,7 +971,7 @@ export class ComposePopupView extends AbstractViewPopup {
// https://github.com/the-djmaze/snappymail/issues/491
tpl.innerHTML = oLastMessage.bodyAsHTML();
tpl.content.querySelectorAll('img').forEach(img => {
img.src || img.dataset.xSrcCid || img.dataset.xSrc || img.replaceWith(img.alt || img.title)
img.src || img.dataset.xSrc || img.replaceWith(img.alt || img.title)
});
sText = tpl.innerHTML.trim();
@ -970,18 +1010,14 @@ export class ComposePopupView extends AbstractViewPopup {
break;
default:
encrypted = PgpUserStore.isEncrypted(sText);
if (encrypted) {
usePlain = PgpUserStore.isEncrypted(sText) || isPlainEditor() || !oLastMessage.isHtml();
if (usePlain) {
sText = oLastMessage.plain();
}
}
this.editor(editor => {
encrypted || editor.setHtml(sText);
if (encrypted || isPlainEditor()) {
editor.modePlain();
}
encrypted && editor.setPlain(sText);
usePlain ? (editor.modePlain() | editor.setPlain(sText)) : editor.setHtml(sText);
this.setSignature(identity, options.mode);
this.setFocusInPopup();
});
@ -1048,7 +1084,7 @@ export class ComposePopupView extends AbstractViewPopup {
}, 100);
}
tryToClose() {
doClose() {
if (AskPopupView.hidden()) {
if (ComposePopupView.inEdit() || (this.isEmptyForm() && !this.draftUid())) {
this.close();
@ -1127,7 +1163,7 @@ export class ComposePopupView extends AbstractViewPopup {
.on('onComplete', (id, result, data) => {
const attachment = this.getAttachmentById(id),
response = data?.Result || {},
errorCode = response.ErrorCode,
errorCode = response.code,
attachmentJson = result && response.Attachment;
let error = '';
@ -1143,7 +1179,7 @@ export class ComposePopupView extends AbstractViewPopup {
.waiting(false)
.uploading(false)
.complete(true)
.error(error + '\n' + response.ErrorMessage);
.error(error + '\n' + response.message);
} else if (attachmentJson) {
attachment
.waiting(false)
@ -1194,7 +1230,7 @@ export class ComposePopupView extends AbstractViewPopup {
});
addShortcut('escape,close', 'shift', ScopeCompose, () => {
this.tryToClose();
this.doClose();
return false;
});
@ -1357,7 +1393,7 @@ export class ComposePopupView extends AbstractViewPopup {
allRecipients() {
return [
// From/sender is also recipient (Sent mailbox)
// this.currentIdentity().email(),
// this.currentIdentity().email,
this.from(),
this.to(),
this.cc(),
@ -1377,7 +1413,8 @@ export class ComposePopupView extends AbstractViewPopup {
key && options.push(['OpenPGP', key]);
key = GnuPGUserStore.getPrivateKeyFor(email, 1);
key && options.push(['GnuPG', key]);
identity.smimeKeyValid() && identity.smimeCertificateValid() && identity.email() === email
identity.smimeKeyValid() && identity.smimeCertificateValid()
&& identity.smimeCertificateChainValid() && identity.email === email
&& options.push(['S/MIME']);
console.dir({signOptions: options});
this.signOptions(options);
@ -1394,22 +1431,22 @@ export class ComposePopupView extends AbstractViewPopup {
OpenPGPUserStore.hasPublicKeyForEmails(recipients)
&& options.push('OpenPGP');
if (await MailvelopeUserStore.hasPublicKeyForEmails(recipients)) {
options.push('Mailvelope');
} else {
'mailvelope' === this.viewArea() && this.bodyArea();
// this.dropMailvelope();
}
const count = recipients.length,
identity = this.currentIdentity(),
from = (identity.smimeKey() && identity.smimeCertificate()) ? identity.email() : null;
from = (identity.smimeKey() && identity.smimeCertificate()) ? identity.email : null;
count
&& count === recipients.filter(email =>
email == from
|| SMimeUserStore.find(certificate => email == certificate.emailAddress && certificate.smimeencrypt)
).length
&& options.push('S/MIME');
if (await MailvelopeUserStore.hasPublicKeyForEmails(recipients)) {
options.push('Mailvelope');
} else {
'mailvelope' === this.viewArea() && this.bodyArea();
// this.dropMailvelope();
}
}
console.dir({encryptOptions:options});
@ -1473,12 +1510,21 @@ export class ComposePopupView extends AbstractViewPopup {
**/
linkedData: []
},
recipients = draft ? [identity.email()] : this.allRecipients(),
recipients = draft ? [identity.email] : this.allRecipients(),
signOptions = !draft && this.doSign() && this.signOptions(),
encryptOptions = this.doEncrypt() && this.encryptOptions(),
isHtml = this.oEditor.isHtml();
if (isHtml) {
tpl.innerHTML = Text;
tpl.content.querySelectorAll('img').forEach(img => {
if (img.dataset.xSrc) {
img.src = img.dataset.xSrc;
img.removeAttribute('data-x-src')
}
});
Text = tpl.innerHTML.trim();
do {
l = Text.length;
Text = Text
@ -1522,12 +1568,11 @@ export class ComposePopupView extends AbstractViewPopup {
alternative.children.push(data);
data = alternative;
}
let sign = true;
let isSigned = false;
for (let i = 0; i < signOptions.length; ++i) {
if ('OpenPGP' == signOptions[i][0]) {
try {
// Doesn't sign attachments
params.html = params.plain = '';
let signed = new MimePart;
signed.headers['Content-Type'] =
'multipart/signed; micalg="pgp-sha256"; protocol="application/pgp-signature"';
@ -1538,47 +1583,52 @@ export class ComposePopupView extends AbstractViewPopup {
signature.headers['Content-Transfer-Encoding'] = '7Bit';
signature.body = await OpenPGPUserStore.sign(data.toString(), signOptions[i][1], 1);
signed.children.push(signature);
isSigned = true;
params.html = params.plain = '';
params.signed = signed.toString();
params.boundary = signed.boundary;
data = signed;
/*
Object.entries(PgpUserStore.getPublicKeyOfEmails([getEmail(this.from())]) || {})
.forEach(([k,v]) => params.publicKey = v);
*/
break;
} catch (e) {
sign = false;
Passphrases.delete(signOptions[i][1])
console.error(e);
}
break;
}
if ('GnuPG' == signOptions[i][0]) {
} else if ('GnuPG' == signOptions[i][0]) {
// TODO: sign in PHP fails
let pass = await GnuPGUserStore.sign(signOptions[i][1]);
if (null != pass) {
// params.signData = data.toString();
params.signFingerprint = signOptions[i][1].fingerprint;
params.signPassphrase = pass;
} else {
sign = false;
// params.attachPublicKey = false;
isSigned = true;
break;
}
break;
}
if ('S/MIME' == signOptions[i][0]) {
} else if ('S/MIME' == signOptions[i][0]) {
// TODO: sign in PHP fails
params.sign = 'S/MIME';
// params.signCertificate = identity.smimeCertificate();
// params.signCertificateChain = identity.smimeCertificateChain();
// params.signPrivateKey = identity.smimeKey();
// params.attachCertificate = false;
if (identity.smimeKeyEncrypted()) {
const pass = await Passphrases.ask(identity,
i18n('SMIME/PRIVATE_KEY_OF', {EMAIL: identity.email()}),
i18n('SMIME/PRIVATE_KEY_OF', {EMAIL: identity.email}),
'CRYPTO/SIGN'
);
if (null != pass) {
params.signPassphrase = pass.password;
// pass.remember && Passphrases.set(identity, pass.password);
} else {
sign = false;
pass.remember && Passphrases.handle(identity, pass.password);
isSigned = true;
}
}
}
}
if (signOptions.length && !sign) {
if (signOptions.length && !isSigned) {
throw 'Signing failed';
}
@ -1587,7 +1637,9 @@ export class ComposePopupView extends AbstractViewPopup {
Object.entries(PgpUserStore.getPublicKeyOfEmails(recipients) || {}).forEach(([k,v]) =>
params.autocrypt.push({
addr: k,
keydata: v.replace(/-----(BEGIN|END) PGP PUBLIC KEY BLOCK-----/g, '').trim()
keydata: v
.replace(/-----(BEGIN|END) PGP PUBLIC KEY BLOCK-----/g, '')
.replace(/\s+/g, '') // Single base64 string, let the server do compliance formatting
})
);
for (let i = 0; i < encryptOptions.length; ++i) {
@ -1607,7 +1659,7 @@ export class ComposePopupView extends AbstractViewPopup {
if ('S/MIME' == encryptOptions[i]) {
params.encryptCertificates = [identity.smimeCertificate()];
SMimeUserStore.forEach(certificate => {
certificate.emailAddress != identity.email()
certificate.emailAddress != identity.email
&& recipients.includes(certificate.emailAddress)
&& params.encryptCertificates.push(certificate.id)
});

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