Compare commits

...

2381 commits

Author SHA1 Message Date
the-djmaze
870df2d861 Resolve #1990 2026-10-24 16:24:28 +02:00
the-djmaze
343ef172d1 Resolve #2008 2026-10-24 14:52:36 +02:00
the-djmaze
bca01170a4 Resolve #2018 2026-10-24 14:48:02 +02:00
the-djmaze
ddf28c299c Require PHP 8.2 2026-10-24 14:12:28 +02:00
the-djmaze
b5c1be34c2 Don't inflate http response by default 2026-10-23 16:45:02 +02:00
Maarten
0204ca48ee
Merge pull request #2088 from JPRauwolf/patch-1
remove opcache installation from docker build
2026-10-23 15:15:59 +02:00
Maarten
0358fdf971
Merge pull request #2087 from FathiBenNasr/fix/header-unfold-encoded-word
MailSo: keep the folding space before an encoded word that follows a plain word
2026-10-23 15:09:28 +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
Jan Philipp Rauwolf
66725386b0
removed opcache installation from docker build
with php 8.5 opcache comes bundled, so the installation fails.
just removing the dependency fixed the build
2026-10-23 00:06:25 +02:00
the-djmaze
e1e94c815d The backtick (`) operator is deprecated, use shell_exec() instead 2026-10-22 23:02:00 +02:00
Fathi Ben Nasr
7c432e392b MailSo: a folded plain-text line no longer leaves two spaces
Unfolding appended a newline before the continuation line, which kept its
own leading whitespace; Header turns the newline into a space, so every
fold became two spaces ("et  checklist"), carried into reply subjects.
The test now compares exactly.
2026-10-22 20:55:38 +00:00
Fathi Ben Nasr
37fcdbe534 MailSo: keep the folding space before an encoded word that follows a plain word
HeaderCollection::Parse() trimmed the folding whitespace in front of every
continuation line starting with an encoded word. RFC 2047 6.2 ignores only
whitespace BETWEEN two adjacent encoded words; after a plain word, RFC 5322
unfolding keeps it. Symfony Mailer folds subjects exactly there, so
"de vos\r\n =?utf-8?Q?=C3=A9quipements?=" was displayed "voséquipements".
The message list (IMAP ENVELOPE) was fine; the opened message was not.

Test: php test/header-unfold.php (fails before the fix).
2026-10-22 20:55:38 +00: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
the-djmaze
9bc56a5abd Ask old login password on CryptKey decrypt failure, to reseal with new password 2024-03-18 12:25:24 +01:00
the-djmaze
a241149b6b Resolve #1450 2024-03-18 02:01:57 +01:00
the-djmaze
b3afbe107d Support S/MIME decrypted opaque signed message #1450 2024-03-17 23:54:54 +01:00
the-djmaze
e2b08ba160 Improved folder/mailbox settings/rename handling 2024-03-17 23:26:32 +01:00
the-djmaze
2cdc8d0055 On folder/mailbox rename, also rename all children instead of reloading all 2024-03-17 19:42:20 +01:00
the-djmaze
195f14ccb8 Bugfix: on folder rename the old fullName must be removed from cache 2024-03-17 18:55:07 +01:00
the-djmaze
7ae80226d8 translate fix (background)-color to (background)color 2024-03-17 17:05:08 +01:00
the-djmaze
2d1a760df3 Resolve #1486 2024-03-17 15:49:36 +01:00
the-djmaze
b99c405202 Hide generate TOTP button when a TOTP exists 2024-03-17 15:17:54 +01:00
the-djmaze
58ad745373 Resolve #1501 2024-03-17 15:10:59 +01:00
the-djmaze
9ccd616132 Now the MainAccount CryptKey is sealed.
This way change-password extension, Oauth2 and others can easily change the cryptkey seal withoug loosing decrypt functionality
2024-03-17 14:53:45 +01:00
the-djmaze
8c033f53fa ChangePassword extensions use \SnappyMail\SensitiveString 2024-03-17 14:50:32 +01:00
the-djmaze
9427b36633 v2.35.4 2024-03-16 01:00:09 +01:00
the-djmaze
5657af7238 Resolve Call to undefined function shell_exec #1496 2024-03-16 00:53:59 +01:00
the-djmaze
89e80bf124 Version bump for #1492 2024-03-16 00:49:18 +01:00
the-djmaze
0fd0d18154 Resolve #1499 2024-03-16 00:36:58 +01:00
the-djmaze
ce63c36fb0 Resolve #1500 2024-03-15 23:54:37 +01:00
the-djmaze
71f78a83bb Cleanup for #1489 2024-03-15 23:41:34 +01:00
Maarten
8f417c53fb
Merge pull request #1498 from SergeyMosin/compact-composer-plugin-v1.0.2
fix: signature editor form is submitted on menu open
2024-03-15 23:38:25 +01:00
Sergey Mosin
7c6d2ad423 fix: signature editor form is submitted on menu open (set menu button element 'type' to 'button') 2024-03-15 11:42:32 -04:00
the-djmaze
c18ae6477d Resolve #1489 2024-03-15 01:16:43 +01:00
the-djmaze
d04f2166d9 OpenPGP.js v5.11.1 2024-03-15 01:11:09 +01:00
Maarten
a0e7bf790e
Merge pull request #1492 from cm-schl/master
Update plugin ldap-mail-accounts: use SensitiveString for passwords
2024-03-15 00:59:42 +01:00
Maarten
df0787d8b0
Merge pull request #1495 from ner00/master
Update pt/pt-PT translation
2024-03-15 00:55:47 +01:00
the-djmaze
bd21c98179 Nextcloud extension added: fetch user email address for login screen 2024-03-15 00:54:41 +01:00
the-djmaze
46bd6d4564 Nextcloud changed stored password handling 2024-03-15 00:53:30 +01:00
lmperfis
c3ca3d7732 Update pt/pt-PT translation 2024-03-14 12:32:42 +00:00
the-djmaze
56950b4b70 Solve PHP Notice: Undefined index: ENCODING 2024-03-14 04:34:15 +01:00
the-djmaze
d0be08ddd3 Bugfix: Nextcloud SignMeType constant incorrect 2024-03-14 04:10:36 +01:00
the-djmaze
106765e384 Nextcloud session['snappymail-password'] to session['snappymail-passphrase'] 2024-03-14 03:59:46 +01:00
the-djmaze
d784c61076 Disable Nextcloud oidc 2024-03-14 03:56:39 +01:00
the-djmaze
f6e3017b50 lowercase login by default 2024-03-13 20:53:50 +01:00
the-djmaze
4fd282dc6c Resolve #1491 2024-03-13 18:48:19 +01:00
cm-schl
a0576dba1f
use SensitiveString for passwords
uses the new SensitiveString class introduced with Snappymail v2.30.0
2024-03-13 17:16:48 +01:00
the-djmaze
e04cc1080a application.ini login_lowercase should be configurable per domain 2024-03-12 22:11:32 +01:00
the-djmaze
4ebb3e7082 Added \SnappyMail\IDN::toAscii() 2024-03-12 21:47:38 +01:00
the-djmaze
aa7323ba43 Lowercase punycode domain names 2024-03-12 19:50:11 +01:00
the-djmaze
d2167dd290 v2.35.3 2024-03-12 18:58:02 +01:00
the-djmaze
891195efa6 Added the demo crypto keys in examples 2024-03-12 18:45:51 +01:00
the-djmaze
295b3e4cfb Skip crashing OpenPGP keys 2024-03-12 18:44:36 +01:00
the-djmaze
03e2dda6c1 OpenPgp generate key support Internationalized Domain Names 2024-03-12 18:14:14 +01:00
the-djmaze
b89f91f08a Delete old domain setup files on save 2024-03-12 18:02:10 +01:00
the-djmaze
4fd2f63c07 Improved more handling of Internationalized Domain Names 2024-03-12 18:01:27 +01:00
the-djmaze
5c21fea5d7 Example how to use PROXYAUTH 2024-03-12 16:22:28 +01:00
the-djmaze
ca249bcced Support Internationalized Domain Names in OpenPGP 2024-03-12 16:20:19 +01:00
the-djmaze
696a2bbd3c Improved handling of Internationalized Domain Names in punycode 2024-03-12 16:06:17 +01:00
the-djmaze
fd54796710 Drop support for IMAP PROXYAUTH as it is a non-standard X-NETSCAPE thing 2024-03-12 03:13:12 +01:00
the-djmaze
521f5e0d08 Cleanup imapsync 2024-03-12 02:31:09 +01:00
the-djmaze
dd662164d0 Upgrade::ConvertInsecureAccounts use new format 2024-03-12 02:30:47 +01:00
the-djmaze
9e562ac882 ValidateWhiteList use IDN punycode 2024-03-12 00:42:09 +01:00
the-djmaze
a093eb3231 oCacher->Get can return NULL 2024-03-11 20:33:34 +01:00
the-djmaze
7999894bda Handle Domain::$name as lowercase punycode due to IDN complexity 2024-03-11 20:11:13 +01:00
the-djmaze
56ede16833 Added: Cache::Exists
Change: Cache::Get can return NULL
2024-03-11 19:54:53 +01:00
the-djmaze
705907e816 Fix Optional parameter $oAccount declared before required parameter $sLogin 2024-03-11 19:38:15 +01:00
the-djmaze
7d66adbe3c Polyfill idn_to_*() 2024-03-11 12:32:59 +01:00
the-djmaze
f726ac7061 Cleanup KeyPathHelper a bit 2024-03-11 11:05:41 +01:00
the-djmaze
df7b11eadb Improved Domain management code 2024-03-11 11:04:19 +01:00
the-djmaze
de150b359c Make Domain::Normalize() private 2024-03-10 23:58:54 +01:00
the-djmaze
1e55f86f51 Deprecate most Domain methods 2024-03-10 23:58:18 +01:00
the-djmaze
ea4cda6a91 bugfix: autoconfig log error 2024-03-10 23:46:46 +01:00
the-djmaze
18a859c68f fix spacing 2024-03-10 23:46:00 +01:00
the-djmaze
cc22327279 Drop deprecated Domain::SetConfig() 2024-03-10 23:21:07 +01:00
the-djmaze
e07749be6a Ignore popups on logoutReload() 2024-03-10 19:09:17 +01:00
the-djmaze
986b3a79f1 Fix Capa handling 2024-03-10 19:00:46 +01:00
Maarten
c450965cb6
Merge pull request #1484 from botsarenthuman/patch-1
Update ImapClient.php
2024-03-10 18:35:59 +01:00
the-djmaze
7459813646 Fix BodyStructure encoding name 2024-03-10 18:25:05 +01:00
the-djmaze
913ed54668 Improved TNEFDecoder 2024-03-10 18:02:38 +01:00
botsarenthuman
84c8e0d08d
Update ImapClient.php
These lines were overwriting our custom plugin which sets it own auth method via a php unshift. These are already the default login methods, so please don't override what someone may have changed in a plugin.
2024-03-09 12:33:52 -08:00
the-djmaze
d498bf2b53 Actions::MainClearFileName() belongs to Raw 2024-03-08 03:09:58 +01:00
the-djmaze
8a3948777f Fix AddressBookProvider namespaces 2024-03-08 02:50:58 +01:00
the-djmaze
9fc4d8f953 Merge Actions::getAdditionalLogParamsByUserLogin() into Actions::LoggerAuthHelper() 2024-03-08 02:49:41 +01:00
the-djmaze
f32f5347a7 Merge Actions::LoggerAuth() into Actions::LoggerAuthHelper() 2024-03-08 02:36:39 +01:00
the-djmaze
6e24f45d4a AddressBookProvider belongs to Contacts actions 2024-03-08 02:34:33 +01:00
the-djmaze
27f5ba9337 Moved some config admin settings 2024-03-07 01:52:48 +01:00
the-djmaze
8e6f35bfad Improved some AppData handling 2024-03-07 01:16:58 +01:00
the-djmaze
565e8f0fb7 draft code for TNEF winmail.dat #1012 2024-03-06 02:31:28 +01:00
the-djmaze
8fe5cd3c95 update mime types 2024-03-06 01:22:49 +01:00
the-djmaze
c1385894cc Make GnuPG separate admin settings option 2024-03-06 01:13:24 +01:00
Maarten
91d9da4046
Merge pull request #1481 from tinola/master
update Polish translation
2024-03-05 20:05:46 +01:00
tinola
ae725b0974 update Polish translation 2024-03-05 10:18:25 +01:00
Maarten
fd5a793de0
Merge pull request #1479 from S-A-L13/master
Disabled small php part of Nextcloud OpenID Connect
2024-03-04 19:52:46 +01:00
Maarten
54723ea522
Merge pull request #1480 from hguilbert/patch-15
Update admin.json
2024-03-04 19:50:04 +01:00
the-djmaze
c8ea6fd2aa Removed TODO as mentioned in #1473 2024-03-04 19:48:57 +01:00
the-djmaze
382ccdb499 Polyfill ctype #1250 2024-03-04 19:46:12 +01:00
hguilbert
2d920396b2
Update admin.json 2024-03-04 19:35:22 +01:00
S-A-L13
b7fc061f04 Merge branch 'master' of https://github.com/S-A-L13/snappymail 2024-03-04 17:08:58 +01:00
S-A-L13
876ed221c3
Update Fork (#2)
* Cleanup OpenPgpImportPopupView code

* update polish translation

* small fix

* Added Import S/MIME certificate popup
And much better handling of the sign and encrypt options

* bugfix: store in Passphrases

* Resolve #1448

* pre-verify S/MIME opaque signed messages so we have a body to view

* Fix timestampToString() for future dates

* Move php8.php to /app/libraries/polyfill/

* Improved Settings handling to prevent bugs in outer code

* Changed AbstractProvider::IsActive() to be abstract

* Example for #1449

* bugfix: previous IsActive() commit

* OpenSSL required due to S/MIME

* Use get_debug_type() instead of gettype()

* update polish translation

* Make all Enumerations classes abstract

* Added search functionality in Admin -> Config
And removed the unused ['capa']['quota']

* Cleanup Quota handling

* OPEN_PGP should be OPENPGP as it is one word

* Improve Capa handling

* Resolve #1451

* Bugfix TypeError: b64Encode(...).match(...) is null

* Small StorageType change

* Bugfix: mailvelope editor failed

* Bugfix: undefined getMailvelopePrivateKeyFor()

* Bugfix: MIME parser RegExp didn't escape `boundary` which caused issues

* Return detailed info on PgpImportKey

* Show GnuPG verify error

* Sort PGP keys by email and id

* Sort S/MIME certificates on emailAddress else validTo

* S/MIME import from signature use `BEGIN PKCS7`

* Optionally use existing private key to generate S/MIME certificate

* Chaned some error_log() to MailSo Logger()

* Force reload of S/MIME certificates list on import

* Make better use of SnappyMail\SensitiveString

* Fix view PGP key button

* Mask all POST data that has a key which contains `pass`

* v2.35.1

* Resolve #1455

* Improved GnuPG error handling

* Update pt/pt-PT translation

* update Polish translation

* Drop support for gnupg pecl extension as it fails with "no passphrase" issues

* Resolve #1456

* Resolve #1458

* v2.35.2

* fix changelog

* Resolve #1461

* Update pt/pt-PT translation

* compact-composer plugin v1.0.0

* Resolve #1462

* Fix decrypt error message

* `new Error()` to `Error()`

* Resolve #1463

* Show url for #1466

* Simplify SignMe/Remember me code

* Simplify language Notifications

* Bugfix: SetPassword expects \SnappyMail\SensitiveString

* https://github.com/the-djmaze/snappymail/issues/1450#issuecomment-1972147950

* improve: fire the 'squire2-toolbar' event after more props are added

* improve: add dark theme support and use 'button' element as menu trigger for consistent styling

* fix: use compact template in non-destructive way (do not replace the PopupsCompose template if a different wysiwyg is used)

* Update admin.json

* Update user.json

* CSS rainloopErrorTip location

* Improved error handling on PGP and S/MIME decrypt

* KnockoutJS remove unused `beforeRemove`

* KnockoutJS drop unused `as`

* KnockoutJS simplify renderMode because only 1 option is used

* KnoutJS cleanup templating.js a bit

* KnockoutJS drop unused `bindingRewriteValidators`

* KnockoutJS drop the twoWayBindings code

* KnockoutJS simplify virtualElements binding check

* KnockoutJS simplify applyBindingsToNodeInternal

* KnockoutJS use Array.isArray

* KnockoutJS drop alias `textinput` for `textInput`

* KnockoutJS scramble `createChildContext`

* KnockoutJS scramble `controlsDescendantBindings`

* KnockoutJS scramble `exportDependencies`

* KnockoutJS drop unused `throttleEvaluation`

* KnockoutJS drop unused `valueAllowUnset`

* KnockoutJS drop unused `templateNodes`

* KnockoutJS drop unused `optionsCaption`

* KnockoutJS drop unused `dontLimitMoves`

* KnockoutJS drop unused `uniqueName`

* KnockoutJS drop IE leftovers

* KnockoutJS drop unused `preprocess`

* KnockoutJS drop unused "disposeWhenNodeIsRemoved" and "disposeWhen"

* KnockoutJS don't scramble exportDependencies. controlsDescendantBindings, createChildContext

* KnockoutJS drop unused `$parentContext` and `$parents`

* KnockoutJS drop unused `$rawData`

* Knockoutjs built latest

* KnockoutJS drop unused template options `nodes`, `if`, `ifnot`

* KnockoutJS use more Array.isArray

* KnockoutJS cleanup code a bit

* KnockoutJS primitiveTypes can just be checked with Object()

* KnockoutJS rebuilt

* Verify S/MIME signed automatically and log Exception

* Automatically verify PGP and S/MIME signed messages

* `new Error` to `Error`

* By default throw AccountNotAllowed as confused in #1478

* GPG use pinentries for decrypt, sign and export

* Better GPG error handling

* GPG show error on view/export

* OpenPGP fix handling of importing keys

* Make "verify signatures automatically" optional, as it requires more IMAP fetching

* S/MIME don't post identity key and certificate, just fetch from server

* Show error to old browsers, instead of crashing

* Automatically verify S/MIME decrypted signed message

---------

Co-authored-by: the-djmaze <>
Co-authored-by: tinola <tinola@poczta.onet.pl>
Co-authored-by: Maarten <3752035+the-djmaze@users.noreply.github.com>
Co-authored-by: lmperfis <joint.striker@gmail.com>
Co-authored-by: Sergey Mosin <sergey@srgdev.com>
Co-authored-by: hguilbert <51283484+hguilbert@users.noreply.github.com>
2024-03-04 17:00:27 +01:00
S-A-L13
3bd073a38b Disabled php part of Nextcloud OpenID Connect
The php code was still executed and throwed the error:
"Undefined array key "snappymail-autologin-oidc" at /var/www/nextloud/nextcloud_28/apps/snappymail/templates/admin-local.php#43"
2024-03-04 16:51:43 +01:00
the-djmaze
34106c937e Automatically verify S/MIME decrypted signed message 2024-03-04 15:33:19 +01:00
the-djmaze
d1466ad2f2 Show error to old browsers, instead of crashing 2024-03-04 15:02:13 +01:00
the-djmaze
574fd820a6 S/MIME don't post identity key and certificate, just fetch from server 2024-03-04 15:01:47 +01:00
the-djmaze
4006dd38f8 Make "verify signatures automatically" optional, as it requires more IMAP fetching 2024-03-04 13:33:40 +01:00
the-djmaze
b5062624fa OpenPGP fix handling of importing keys 2024-03-04 12:42:23 +01:00
the-djmaze
5aa71a3ede GPG show error on view/export 2024-03-04 11:15:34 +01:00
the-djmaze
d57300dd75 Better GPG error handling 2024-03-04 11:02:12 +01:00
the-djmaze
464d22d69a GPG use pinentries for decrypt, sign and export 2024-03-04 10:14:14 +01:00
the-djmaze
7d485813e5 By default throw AccountNotAllowed as confused in #1478 2024-03-04 10:12:59 +01:00
the-djmaze
644c8ad389 new Error to Error 2024-03-04 01:12:40 +01:00
the-djmaze
870019d2df Automatically verify PGP and S/MIME signed messages 2024-03-04 01:07:00 +01:00
the-djmaze
27fbe01876 Verify S/MIME signed automatically and log Exception 2024-03-03 23:52:02 +01:00
the-djmaze
b2009f6bc0 KnockoutJS rebuilt 2024-03-03 23:10:16 +01:00
the-djmaze
9db2690710 KnockoutJS primitiveTypes can just be checked with Object() 2024-03-03 22:47:19 +01:00
the-djmaze
0e1a34f067 KnockoutJS cleanup code a bit 2024-03-03 22:36:03 +01:00
the-djmaze
b07bf3ad12 KnockoutJS use more Array.isArray 2024-03-03 22:33:29 +01:00
the-djmaze
1357a696be KnockoutJS drop unused template options nodes, if, ifnot 2024-03-03 21:32:53 +01:00
the-djmaze
e97cf8fa6b Knockoutjs built latest 2024-03-03 19:20:51 +01:00
the-djmaze
e3f781cffb KnockoutJS drop unused $rawData 2024-03-03 19:20:16 +01:00
the-djmaze
ca170f2461 KnockoutJS drop unused $parentContext and $parents 2024-03-03 19:17:26 +01:00
the-djmaze
766d3eaa98 KnockoutJS don't scramble exportDependencies. controlsDescendantBindings, createChildContext 2024-03-03 19:10:00 +01:00
the-djmaze
a8e38a998f KnockoutJS drop unused "disposeWhenNodeIsRemoved" and "disposeWhen" 2024-03-03 18:23:52 +01:00
the-djmaze
12711be049 KnockoutJS drop unused preprocess 2024-03-03 18:18:26 +01:00
the-djmaze
4276896002 KnockoutJS drop IE leftovers 2024-03-03 18:17:01 +01:00
the-djmaze
39ad99224e KnockoutJS drop unused uniqueName 2024-03-03 18:12:18 +01:00
the-djmaze
62bda28a28 KnockoutJS drop unused dontLimitMoves 2024-03-03 18:11:15 +01:00
the-djmaze
acf1be454f KnockoutJS drop unused optionsCaption 2024-03-03 18:01:02 +01:00
the-djmaze
bb3fc79e09 KnockoutJS drop unused templateNodes 2024-03-03 17:56:43 +01:00
the-djmaze
ab1a75935f KnockoutJS drop unused valueAllowUnset 2024-03-03 17:36:04 +01:00
the-djmaze
05b1e96046 KnockoutJS drop unused throttleEvaluation 2024-03-03 17:31:44 +01:00
the-djmaze
2876f92f4d KnockoutJS scramble exportDependencies 2024-03-03 17:30:15 +01:00
the-djmaze
667e50c9fb KnockoutJS scramble controlsDescendantBindings 2024-03-03 17:27:18 +01:00
the-djmaze
4ee00d30ae KnockoutJS scramble createChildContext 2024-03-03 17:24:55 +01:00
the-djmaze
47cd902369 KnockoutJS drop alias textinput for textInput 2024-03-03 17:21:27 +01:00
the-djmaze
ee3aa16fcf KnockoutJS use Array.isArray 2024-03-03 17:12:35 +01:00
the-djmaze
a40e6484a1 KnockoutJS simplify applyBindingsToNodeInternal 2024-03-03 17:02:38 +01:00
the-djmaze
33f0dfd3b2 KnockoutJS simplify virtualElements binding check 2024-03-03 16:58:07 +01:00
the-djmaze
4af874e1d1 KnockoutJS drop the twoWayBindings code 2024-03-03 16:49:52 +01:00
the-djmaze
df8f13e359 KnockoutJS drop unused bindingRewriteValidators 2024-03-03 16:48:42 +01:00
the-djmaze
0f0f60e456 KnoutJS cleanup templating.js a bit 2024-03-03 16:46:54 +01:00
the-djmaze
97a369048e KnockoutJS simplify renderMode because only 1 option is used 2024-03-03 16:41:39 +01:00
the-djmaze
dde1183174 KnockoutJS drop unused as 2024-03-03 16:27:21 +01:00
the-djmaze
513fc949d7 KnockoutJS remove unused beforeRemove 2024-03-03 16:09:07 +01:00
the-djmaze
9e05aa4a50 Merge branch 'master' of github.com:the-djmaze/snappymail 2024-03-03 04:15:07 +01:00
the-djmaze
41969bf2c1 Improved error handling on PGP and S/MIME decrypt 2024-03-03 04:14:49 +01:00
the-djmaze
0e202a6640 CSS rainloopErrorTip location 2024-03-03 03:13:03 +01:00
Maarten
4d2d223bdb
Merge pull request #1475 from hguilbert/patch-14
Update user.json
2024-03-01 20:01:50 +01:00
hguilbert
ae8d40786e
Update user.json 2024-03-01 19:16:59 +01:00
Maarten
1cff9d987e
Merge pull request #1474 from hguilbert/patch-13
Update admin.json
2024-03-01 19:11:38 +01:00
Maarten
380eae6669
Merge pull request #1473 from SergeyMosin/compact-composer-plugin-v1.0.1
Compact composer plugin v1.0.1
2024-03-01 19:10:10 +01:00
hguilbert
8e0414a4ec
Update admin.json 2024-03-01 19:00:08 +01:00
Sergey Mosin
d6acf55ab2 fix: use compact template in non-destructive way (do not replace the PopupsCompose template if a different wysiwyg is used) 2024-03-01 10:20:45 -05:00
Sergey Mosin
6169f6083f improve: add dark theme support and use 'button' element as menu trigger for consistent styling 2024-03-01 10:12:24 -05:00
Sergey Mosin
c0247ff2bd improve: fire the 'squire2-toolbar' event after more props are added 2024-03-01 10:07:14 -05:00
the-djmaze
2efe90fe48 https://github.com/the-djmaze/snappymail/issues/1450#issuecomment-1972147950 2024-03-01 02:08:29 +01:00
the-djmaze
d84f39be86 Bugfix: SetPassword expects \SnappyMail\SensitiveString 2024-03-01 01:45:56 +01:00
the-djmaze
c1b7d37663 Simplify language Notifications 2024-03-01 01:39:00 +01:00
the-djmaze
5b3b362012 Simplify SignMe/Remember me code 2024-02-28 20:42:31 +01:00
the-djmaze
490465cbbf Show url for #1466 2024-02-28 20:12:15 +01:00
Maarten
e880d0ce86
Merge pull request #1466 from SergeyMosin/compact-composer-plugin
compact-composer plugin v1.0.0
2024-02-28 20:10:12 +01:00
Maarten
0b092a588d
Merge pull request #1465 from ner00/master
Update pt/pt-PT translation
2024-02-28 20:09:54 +01:00
the-djmaze
593fd4c41d Resolve #1463 2024-02-28 20:09:16 +01:00
the-djmaze
14bee5d3bd new Error() to Error() 2024-02-28 19:46:21 +01:00
the-djmaze
4303ee1908 Merge branch 'master' of github.com:the-djmaze/snappymail 2024-02-28 19:45:30 +01:00
the-djmaze
df963e3537 Fix decrypt error message 2024-02-28 19:45:14 +01:00
the-djmaze
6e56808a0d Resolve #1462 2024-02-28 19:36:57 +01:00
Sergey Mosin
57003ab4b8 compact-composer plugin v1.0.0 2024-02-28 09:47:39 -05:00
lmperfis
a95ecf6133 Update pt/pt-PT translation 2024-02-28 14:18:01 +00:00
Maarten
47793ce1cb
Merge pull request #1460 from tinola/master
update Polish translation
2024-02-27 23:17:53 +01:00
the-djmaze
b3893f7164 Resolve #1461 2024-02-27 23:16:47 +01:00
the-djmaze
0cd39afadc fix changelog 2024-02-27 23:08:42 +01:00
the-djmaze
37dee0e78e v2.35.2 2024-02-27 23:04:32 +01:00
the-djmaze
7e6523b59a Resolve #1458 2024-02-27 22:51:10 +01:00
Maarten
1722d88f1a
Merge pull request #1454 from S-A-L13/master
Added function for ldap-identities plugin to filter by mail prefixes (allow compatibility for e.g. Exchange and others)
2024-02-27 22:34:48 +01:00
Maarten
44bda3a315
Merge pull request #1457 from ner00/master
Update pt/pt-PT translation
2024-02-27 22:33:06 +01:00
the-djmaze
719b21a4fb Resolve #1456 2024-02-27 22:30:29 +01:00
the-djmaze
68ed7cafae Drop support for gnupg pecl extension as it fails with "no passphrase" issues 2024-02-27 22:29:36 +01:00
tinola
6bb77d6d7c update Polish translation 2024-02-27 21:19:50 +01:00
lmperfis
150d95c157 Update pt/pt-PT translation 2024-02-27 18:00:43 +00:00
the-djmaze
3d176d4e4f Improved GnuPG error handling 2024-02-27 18:45:25 +01:00
the-djmaze
c73e3637a3 Resolve #1455 2024-02-27 15:12:42 +01:00
S-A-L13
31d9e9fd1b Changed version to 2.3 2024-02-27 14:30:57 +01:00
S-A-L13
20f6dea345 minor changes 2024-02-27 13:46:02 +01:00
S-A-L13
0763f3d1c6 Removed wrong escapes 2024-02-27 13:22:38 +01:00
S-A-L13
f4554374bd Minor changes to match coding style of original coder 2024-02-27 13:10:53 +01:00
the-djmaze
b0211424a7 v2.35.1 2024-02-27 12:05:30 +01:00
S-A-L13
ebf7540554 Added function CleanupMailAddresses
Added function (and additional code to start it) that helps to clean up the mail addresses array. It removes the prefix (if set in config) and removes all addresses with no or a different prefix.
2024-02-27 12:04:07 +01:00
the-djmaze
275ad40346 Mask all POST data that has a key which contains pass 2024-02-27 11:25:34 +01:00
the-djmaze
28d8bec935 Fix view PGP key button 2024-02-27 11:04:35 +01:00
the-djmaze
7cacc9b503 Make better use of SnappyMail\SensitiveString 2024-02-27 11:03:36 +01:00
the-djmaze
0f7505baba Force reload of S/MIME certificates list on import 2024-02-27 02:41:16 +01:00
the-djmaze
847d33abc6 Chaned some error_log() to MailSo Logger() 2024-02-27 02:31:53 +01:00
the-djmaze
182f884374 Optionally use existing private key to generate S/MIME certificate 2024-02-27 02:19:12 +01:00
the-djmaze
97b65465e2 S/MIME import from signature use BEGIN PKCS7 2024-02-26 22:34:15 +01:00
the-djmaze
20daa1ce13 Sort S/MIME certificates on emailAddress else validTo 2024-02-26 19:34:50 +01:00
the-djmaze
091895df38 Sort PGP keys by email and id 2024-02-26 19:21:40 +01:00
the-djmaze
6141ca7936 Show GnuPG verify error 2024-02-26 18:43:17 +01:00
the-djmaze
6f98aee998 Return detailed info on PgpImportKey 2024-02-26 18:26:51 +01:00
the-djmaze
7bc302c665 Bugfix: MIME parser RegExp didn't escape boundary which caused issues 2024-02-26 17:36:55 +01:00
the-djmaze
50569e1854 Bugfix: undefined getMailvelopePrivateKeyFor() 2024-02-26 17:17:32 +01:00
the-djmaze
0cbca605ec Bugfix: mailvelope editor failed 2024-02-26 16:59:50 +01:00
the-djmaze
ab748aab99 Small StorageType change 2024-02-26 16:44:25 +01:00
the-djmaze
2559c09b23 Bugfix TypeError: b64Encode(...).match(...) is null 2024-02-26 16:42:35 +01:00
S-A-L13
12cbb9f07e Added config fields for mail prefix
Mail prefix is needed to filter mailidentities. For example suche set by Exchange in the ProxyAddresses field, which can hold different address types
2024-02-26 15:47:57 +01:00
the-djmaze
99b7ec661c Resolve #1451 2024-02-26 14:53:25 +01:00
Maarten
b4e146776f
Merge pull request #1452 from tinola/master
update polish translation
2024-02-26 13:51:40 +01:00
the-djmaze
118587dcf3 Improve Capa handling 2024-02-26 13:48:57 +01:00
the-djmaze
6e1aae3f61 OPEN_PGP should be OPENPGP as it is one word 2024-02-26 13:39:09 +01:00
the-djmaze
9006c377fd Cleanup Quota handling 2024-02-26 13:36:10 +01:00
the-djmaze
886e53bb8f Added search functionality in Admin -> Config
And removed the unused ['capa']['quota']
2024-02-26 13:26:06 +01:00
the-djmaze
e1dced8007 Make all Enumerations classes abstract 2024-02-26 11:12:02 +01:00
tinola
cbfc7e7d82 update polish translation 2024-02-26 07:25:09 +01:00
the-djmaze
9ef11db895 Use get_debug_type() instead of gettype() 2024-02-26 04:20:28 +01:00
the-djmaze
044b0dcde6 OpenSSL required due to S/MIME 2024-02-26 03:16:18 +01:00
the-djmaze
dcfa144ce3 bugfix: previous IsActive() commit 2024-02-26 02:58:06 +01:00
the-djmaze
58d3fc8fd5 Example for #1449 2024-02-26 02:54:44 +01:00
the-djmaze
cfc497556e Changed AbstractProvider::IsActive() to be abstract 2024-02-26 02:49:06 +01:00
the-djmaze
007e4b25eb Improved Settings handling to prevent bugs in outer code 2024-02-26 02:47:56 +01:00
the-djmaze
c6aad25581 Move php8.php to /app/libraries/polyfill/ 2024-02-25 20:13:39 +01:00
the-djmaze
5262e59fdd Merge branch 'master' of github.com:the-djmaze/snappymail 2024-02-25 13:11:30 +01:00
the-djmaze
57ce6f9bbf Fix timestampToString() for future dates 2024-02-25 13:11:17 +01:00
Maarten
e3830a5c3f
Merge pull request #1445 from S-A-L13/master
StartTLS for LDAP Identities plugin
2024-02-25 12:58:00 +01:00
the-djmaze
5552d3c1b9 pre-verify S/MIME opaque signed messages so we have a body to view 2024-02-25 12:56:25 +01:00
the-djmaze
a98f8aa1f6 Merge branch 'master' of github.com:the-djmaze/snappymail 2024-02-25 11:38:33 +01:00
the-djmaze
1e03eac9a5 Resolve #1448 2024-02-25 11:38:19 +01:00
Maarten
8aef85804e
Merge pull request #1447 from tinola/master
update polish translation
2024-02-25 11:31:57 +01:00
Maarten
2b0ca5d314
Merge branch 'master' into master 2024-02-25 11:31:49 +01:00
the-djmaze
a7041b4efb bugfix: store in Passphrases 2024-02-23 03:22:29 +01:00
the-djmaze
6f33bc23d3 Added Import S/MIME certificate popup
And much better handling of the sign and encrypt options
2024-02-23 03:14:36 +01:00
tinola
6ac5cea2e4 small fix 2024-02-22 17:51:20 +01:00
tinola
70a7784d68 update polish translation 2024-02-22 17:32:39 +01:00
S-A-L13
8dac28e1a2 Update index.php 2024-02-22 14:56:29 +01:00
S-A-L13
383e40636b Added StartTLS functionality
Added StartTLS in function Connect().
And also added parameters in config to activate StartTLS.
2024-02-22 14:54:55 +01:00
the-djmaze
f94fc34d4f Cleanup OpenPgpImportPopupView code 2024-02-22 00:33:20 +01:00
the-djmaze
0efc0bbe3a Translate some S/MIME text 2024-02-22 00:00:12 +01:00
the-djmaze
453e73f71a Cache S/MIME passphrases when "remember" is checked 2024-02-21 23:28:53 +01:00
the-djmaze
9941ff61f7 Bugfix: S/MIME sigPart handling 2024-02-21 01:28:36 +01:00
the-djmaze
aebed32f76 Bugfix: verify S/MIME signatures got broken allong the way while implementing this 2024-02-21 01:23:36 +01:00
the-djmaze
d5d47ffed1 Use SensitiveString at more places 2024-02-21 00:59:09 +01:00
the-djmaze
a473e5e223 Implement \Stringable 2024-02-21 00:36:31 +01:00
the-djmaze
d5a43e422d v2.35.0 2024-02-20 18:58:18 +01:00
the-djmaze
e9a63b40c5 Enable S/MIME encrypt #259 2024-02-20 18:29:35 +01:00
the-djmaze
ca12655e5f Cleanup PGP PUBLIC KEY handling 2024-02-20 18:14:32 +01:00
the-djmaze
6ce08726b4 Automaticaly import S/MIME certificates that are found in signatures #259 2024-02-20 17:42:26 +01:00
the-djmaze
3d168b4688 Cleanup DoPgpVerifyMessage action param handling 2024-02-20 16:03:47 +01:00
the-djmaze
5a2d2fd0e5 Handle S/MIME encrypt, decrypt, sign and very everywhere #259 2024-02-20 16:03:22 +01:00
the-djmaze
44a623543f set smimeKey and smimeCertificate for demo 2024-02-20 15:21:32 +01:00
the-djmaze
18d0ba4acb Get S/MIME decrypt working for #259 2024-02-20 13:04:12 +01:00
the-djmaze
1762198642 Get S/MIME encryption working #259 2024-02-20 12:38:57 +01:00
the-djmaze
196dffa8d8 Fix S/MIME certificates cache handling 2024-02-20 11:46:34 +01:00
the-djmaze
4b8a295fbe mask another passphrase in the logs for PHP < 8.2 2024-02-20 11:24:23 +01:00
the-djmaze
ec0d92bfac mask passphrases in logs for PHP < 8.2 2024-02-20 11:22:28 +01:00
the-djmaze
479452ad7b Cache S/MIME certificates info #259 2024-02-20 11:11:10 +01:00
the-djmaze
f5f3a90275 Only ask S/MIME private key passphrase when it is encrypted #259 2024-02-20 11:10:43 +01:00
Maarten
f06b6edef5
Merge pull request #1443 from tinola/master
update Polish translations
2024-02-20 10:34:09 +01:00
tinola
0063c2fc3f update Polish translations 2024-02-20 08:11:15 +01:00
the-djmaze
7ae53719c4 Rename CSS .openpgp-control to .crypto-control 2024-02-20 01:52:37 +01:00
the-djmaze
9c9adde9c8 Added some missing translations 2024-02-20 01:09:41 +01:00
the-djmaze
568caed3c7 Ask password to generate S/MIME self signed #259 2024-02-20 01:01:15 +01:00
the-djmaze
3f08051e37 Added a S/MIME certificates list to User settings #259 2024-02-19 20:16:37 +01:00
the-djmaze
a22ae5fcf4 fix name addSMimeEncrypted 2024-02-19 18:54:00 +01:00
the-djmaze
10ec90435d Show certificate icon for p12 and pfx files 2024-02-19 18:51:52 +01:00
the-djmaze
2f2079e9c1 Rename SearchPGPKey action to PgpSearchKey 2024-02-19 17:39:56 +01:00
the-djmaze
2fa3508c11 Rename MessagePgpVerify action to PgpVerifyMessage 2024-02-19 17:35:48 +01:00
the-djmaze
55e55b7bb7 Rename SMime actions 2024-02-19 17:33:24 +01:00
the-djmaze
9a5a5091a1 More S/MIME changes for #259 2024-02-19 15:35:10 +01:00
the-djmaze
a9b60a762f Also mark p7m with certificate icon 2024-02-19 15:29:25 +01:00
the-djmaze
f181863d50 No default passphrase for self-signed #259 2024-02-19 15:28:03 +01:00
the-djmaze
f53d9ed6c3 Verify S/MIME signed messages #259 2024-02-19 15:14:11 +01:00
the-djmaze
4df3766231 Fix signing messages with S/MIME #259 2024-02-19 12:51:02 +01:00
the-djmaze
128e2f6254 Added signing using S/MIME #259 2024-02-19 04:35:37 +01:00
the-djmaze
f83b60782a Improved handling to create S/MIME certificates #259 2024-02-19 01:52:12 +01:00
the-djmaze
6fa4dff23b Set S/MIME private key and certificate per identity #259 2024-02-18 17:18:47 +01:00
the-djmaze
33de51b2d5 Check for CONDSTORE or QRESYNC to get the HIGHESTMODSEQ 2024-02-18 16:47:28 +01:00
the-djmaze
8a10d30f2a Disable IMAP METADATA by default (hardly used) 2024-02-18 16:47:04 +01:00
the-djmaze
8a0b3d3791 Resolve #1425 2024-02-18 16:44:20 +01:00
the-djmaze
86f084e5a9 Added Auth Bearer to SnappyMail\HTTP\Request 2024-02-18 16:29:25 +01:00
the-djmaze
785bde153e MMS examples of #1294 2024-02-18 16:13:59 +01:00
the-djmaze
96d08cd418 Added option to create self-signed S/MIME certificate #259 2024-02-18 16:08:09 +01:00
the-djmaze
ae4a90bda5 Bugfix: when moving a folder/mailbox check for parent delimiter 2024-02-18 14:28:42 +01:00
the-djmaze
859b064d39 More info about IMAP capabilities 2024-02-18 13:04:05 +01:00
the-djmaze
dd5bb8593c Resolve #1442 2024-02-18 10:50:33 +01:00
the-djmaze
53940e64ab Added more code for S/MIME #259 2024-02-16 01:23:23 +01:00
the-djmaze
dceb5fca41 Resolve #1439 2024-02-15 23:56:26 +01:00
the-djmaze
5a69d1e241 Resolve #1438 2024-02-15 23:45:43 +01:00
the-djmaze
e02cb38974 v2.34.2 2024-02-14 18:55:04 +01:00
the-djmaze
662121d90e Resolve #1433 2024-02-14 18:48:13 +01:00
the-djmaze
2dbcd47a56 Resolve #1432 2024-02-14 18:34:58 +01:00
the-djmaze
7b3102a6b3 Added isSMimeEncrypted() and isSMimeSigned() #259 2024-02-14 02:43:54 +01:00
the-djmaze
3648b3eb60 Resolve #64 2024-02-13 19:44:40 +01:00
the-djmaze
72815f842e Small bugfix for the folder editor popup 2024-02-13 18:25:17 +01:00
the-djmaze
72b481f9c4 Fix english text for ERROR_ON_SAVING_FILE 2024-02-13 18:00:33 +01:00
the-djmaze
6982b57289 Small MailSo cleanup 2024-02-13 17:45:53 +01:00
the-djmaze
f89a1cd3f9 v2.34.1 2024-02-13 14:58:32 +01:00
the-djmaze
b3e1fe1320 Resolve #1430 2024-02-13 14:53:20 +01:00
the-djmaze
9f2b2e50d7 v2.34.0 2024-02-13 13:24:45 +01:00
the-djmaze
e4c7cec7e3 Disable ACL myRights as it is in the Settings -> Folder popup #157 2024-02-13 11:42:46 +01:00
the-djmaze
39c17b6493 Make whole settings folder td clickable 2024-02-13 11:16:07 +01:00
the-djmaze
312f1c3b9b use Folder.unreadEmails() instead of Folder.unreadCount() 2024-02-13 11:11:19 +01:00
the-djmaze
27d119c068 Bugfix: typeCast() number could return NaN 2024-02-13 11:10:56 +01:00
the-djmaze
2a14566679 friendlySize() could return NaN 2024-02-13 10:50:31 +01:00
the-djmaze
306d3ec7b5 Improved ACL handling #157 2024-02-13 10:44:38 +01:00
the-djmaze
7e304daee0 Bugfix: this.error() undefined when hiding folder edit message 2024-02-13 02:17:27 +01:00
the-djmaze
304c67534b Added: support all namespaces 2024-02-13 02:05:38 +01:00
the-djmaze
de7e9d86d3 bugfix: undefined $keyId 2024-02-12 22:30:29 +01:00
the-djmaze
e727c54798 Fix folder ACL checkboxes #157 2024-02-12 20:54:51 +01:00
the-djmaze
32e3f829fa Cleaner ACL list 2024-02-12 20:52:30 +01:00
the-djmaze
b0d4b56cee Some improvements for ACL Support #157 2024-02-12 20:48:43 +01:00
the-djmaze
a0f7114a39 Bugfix: KnockoutJS failed to output text '0' 2024-02-12 20:07:08 +01:00
the-djmaze
905315b164 Bugfix: friendlySize() failed on 0 2024-02-12 20:03:06 +01:00
the-djmaze
ec111defbb Process all IMAP namespaces 2024-02-12 17:58:24 +01:00
the-djmaze
ba55b328ea Workaround Dovecot PREAUTH [CAPABILITY (null)] issue 2024-02-12 17:15:53 +01:00
the-djmaze
7ab9d1a980 Workaround disabled ACL could cause "Disconnected: Too many invalid IMAP commands" #157 2024-02-12 17:14:38 +01:00
the-djmaze
3c464cace5 Resolve #1384 2024-02-12 13:37:51 +01:00
the-djmaze
9933a0e83e Added draft code to handle S/MIME through GnuPG #259 2024-02-12 13:36:12 +01:00
the-djmaze
9cceafcb3e Enable OpenPGP by default #89 2024-02-12 10:35:46 +01:00
the-djmaze
2ed6ccf89c Bugfix: new identity PGP settings incorrect value assignment 2024-02-12 10:32:10 +01:00
the-djmaze
8ab32816e2 Resolve #1404 2024-02-12 02:13:39 +01:00
the-djmaze
355f6158be Bugfix: #342 2024-02-12 01:26:56 +01:00
the-djmaze
7da5692865 Resolve #342 2024-02-12 01:01:19 +01:00
the-djmaze
a45cd5904f Trim white space in PGP armored key #89 2024-02-11 22:09:11 +01:00
the-djmaze
c72c2dbaa4 Added options to Identity for PGP sign and encrypt #89 2024-02-11 21:16:38 +01:00
the-djmaze
4c84c6cdfc Resolve #1427 2024-02-11 20:48:03 +01:00
the-djmaze
ec5d570ac6 Added ko toggle bindingHandler 2024-02-11 20:34:02 +01:00
the-djmaze
17cb093b32 Automaticaly add PGP key from Autocrypt #89 2024-02-11 19:23:09 +01:00
the-djmaze
e28085fbba Added deprecated MIME PGP headers for #89 ? 2024-02-11 18:21:38 +01:00
the-djmaze
62f0bac381 Allow CSP connect-src CORS for keys.openpgp.org to directly fetch PGP keys 2024-02-11 18:08:27 +01:00
the-djmaze
bfb0874c47 Fix for #1427 2024-02-11 17:06:22 +01:00
the-djmaze
7f40c317d0 Define JMAP FolderModel.myRights 2024-02-11 17:00:09 +01:00
the-djmaze
ea55eb13e5 Centralize loading of accounts and identities 2024-02-11 16:40:35 +01:00
the-djmaze
7c1f70c51e Replace some ko.exportSymbol('*') in favour of ko['*']
and drop ko.exportProperty()
2024-02-11 15:37:24 +01:00
the-djmaze
6c144b5a28 Improved handling of visible folders 2024-02-11 15:03:19 +01:00
the-djmaze
7ca5991041 KnockoutJS use Symbol for isObservableArray() 2024-02-11 14:44:47 +01:00
the-djmaze
5225b62a0a Simplify generating folderListVisible 2024-02-11 04:31:32 +01:00
the-djmaze
bb906e61ab Make time_zone a select list due to PEBKAC #1417 #1426 2024-02-11 04:10:30 +01:00
the-djmaze
e1a51ba69c Add idea for #1427 2024-02-11 03:52:17 +01:00
the-djmaze
a25d35d6df Resolve #1405 2024-02-09 02:39:00 +01:00
the-djmaze
d1154df74d Resolve #1415 2024-02-08 18:48:54 +01:00
the-djmaze
81f8fe3019 Resolve #1425 2024-02-08 18:12:32 +01:00
the-djmaze
43cbbd51c1 Also allow backup PGP keys on server on import #89 2024-02-08 18:01:15 +01:00
the-djmaze
83aa65981b Added link to schema.org #1422 2024-02-08 17:41:50 +01:00
the-djmaze
a056631cef Update example extension/plugin 2024-02-08 00:46:16 +01:00
the-djmaze
89b6e84fea Deduplicate and sort OpenPGP.js key stores #89 2024-02-08 00:08:24 +01:00
the-djmaze
d832117ae4 Disabled support for Nextcloud OpenID Connect
https://github.com/the-djmaze/snappymail/issues/1420#issuecomment-1933045917
2024-02-07 23:35:45 +01:00
the-djmaze
8f037b4b46 Use input type="search" 2024-02-07 02:42:00 +01:00
the-djmaze
a7f764546f Fix pgp import button style #89 2024-02-07 02:38:46 +01:00
the-djmaze
f8002151be Added button to import keys from server into OpenPGP.js #89 2024-02-07 02:29:22 +01:00
the-djmaze
6c01db395d Added support to lookup public keys online #89 2024-02-07 01:55:52 +01:00
the-djmaze
bac6c2d332 More log debug messages for #1420 2024-02-07 01:17:26 +01:00
the-djmaze
59fea00a31 Redesign Nextcloud OIDC login #1420 2024-02-06 20:31:50 +01:00
the-djmaze
66c9091b88 Add more debug messages for #1420 2024-02-06 19:57:00 +01:00
the-djmaze
f0d97b52d9 Support JSON-LD #1422 2024-02-06 19:52:00 +01:00
the-djmaze
a46484755b Added Autoconfig as extension 2024-02-06 18:43:33 +01:00
the-djmaze
728286d16c Cleanup some mail address parsing 2024-02-06 18:31:11 +01:00
the-djmaze
04a78ad8bd Resolve #1294 2024-02-06 17:00:48 +01:00
the-djmaze
d55ce60cc5 Resolve #1225 by using optional link tracking 2024-02-06 16:05:17 +01:00
the-djmaze
d2c59d2e1c Change for #1420 2024-02-06 14:24:56 +01:00
the-djmaze
19946fc551 Let Autoconfig also use Microsoft's autodiscover 2024-02-06 14:17:36 +01:00
the-djmaze
d6be75e39d Messages don't need generateUid() either 2024-02-06 12:09:17 +01:00
the-djmaze
ee6654474d No use for contacts generateUid() 2024-02-06 12:07:06 +01:00
the-djmaze
453e9b0f3e Resolve #1411 2024-02-06 12:05:30 +01:00
the-djmaze
e5e6ad8aac Merge IMAP login SCRAM* with PLAIN 2024-02-06 11:50:47 +01:00
Maarten
c365ffedf3
Merge pull request #1424 from tinola/master
Update admin.json
2024-02-06 11:06:17 +01:00
tinola
dc2a72c756
Update admin.json 2024-02-06 07:19:01 +01:00
the-djmaze
83659ff62d Drop the bSearchSecretWords param from logger 2024-02-05 22:30:23 +01:00
the-djmaze
e1d95f8632 Changes for #1420 to detect OIDC 2024-02-05 21:40:30 +01:00
the-djmaze
cbcab5b981 Bugfix: domain autoconfig regular expression is too large 2024-02-05 21:29:25 +01:00
the-djmaze
3ec93d11ea Bugfix domain autoconfig error 2024-02-05 20:52:33 +01:00
the-djmaze
8c3a4b4fc7 Improved style of Admin -> Domain -> Domain description 2024-02-05 20:42:01 +01:00
the-djmaze
8948d22095 This might solve #1420 ? 2024-02-05 20:16:25 +01:00
the-djmaze
5e0d52b9e7 Added domain autconfig detection of @benbucksch
https://datatracker.ietf.org/doc/draft-bucksch-autoconfig/
2024-02-05 19:56:58 +01:00
the-djmaze
d4cdfb8fa5 Added draft code for FUZZY search RFC 6851 2024-02-05 16:12:56 +01:00
the-djmaze
a9669e430f Added FlightReservation example of @vkrause for #1422 2024-02-05 15:27:13 +01:00
the-djmaze
be633f78a7 Add some basic support for Linked Data (Structured Email) #1422 2024-02-05 14:52:23 +01:00
the-djmaze
ea07ea45b8 Move docker examples to sub directory 2024-02-05 14:26:23 +01:00
the-djmaze
809fca73a0 Extract WYSIWYG code from Html.js into HtmlEditor.js 2024-02-05 14:10:58 +01:00
the-djmaze
2084913a4d Rename moment to time as it has nothing todo anymore with momentjs
Change some observables to static as they never change later
2024-02-05 01:15:05 +01:00
the-djmaze
673126ae8a Resolve #1416 2024-02-05 00:36:07 +01:00
the-djmaze
b35eab77be Merge branch 'master' of github.com:the-djmaze/snappymail 2024-02-04 23:49:14 +01:00
the-djmaze
c04a5da865 Uncaught exception: Exception: DateTimeZone::__construct(): Unknown or bad timezone (UTC+1) #1417 2024-02-04 23:48:58 +01:00
the-djmaze
a24a7dbde7
Merge pull request #1418 from tinola/master
Update Polish translation
2024-02-04 23:35:29 +01:00
the-djmaze
be8d302b82 Improved MIME email addresses parse info 2024-02-04 23:25:31 +01:00
the-djmaze
ec9197cb85 Also fix MIME header parsing in JavaScript, read issue #1403 2024-02-04 23:03:53 +01:00
the-djmaze
0914ede3a3 Cleanup Logger a bit 2024-02-03 12:28:06 +01:00
the-djmaze
f105ed3f9d Changed Knockout a bit 2024-02-03 12:06:56 +01:00
tinola
84aa997c62 Update Polish translation 2024-02-02 08:26:02 +01:00
the-djmaze
a352ebff25 Changes for #1412 2024-01-30 19:16:53 +01:00
the-djmaze
9eb5646aec Redesign WYSIWYG handling to allow any editor (quill, tiny, etc. etc.) 2024-01-30 18:16:39 +01:00
the-djmaze
e6db60e76c
Merge pull request #1407 from ner00/master
Update pt/pt-PT translation
2024-01-29 11:31:18 +01:00
the-djmaze
070d10a1d0 Resolve #1408 2024-01-29 11:25:42 +01:00
the-djmaze
0a6ac6613c Resolve #1409 2024-01-29 11:22:19 +01:00
lmperfis
7eba9cf48e Update pt/pt-PT translation 2024-01-25 16:04:40 +00:00
the-djmaze
3a37aad25f Remove unused NameWithDelimitrom() 2024-01-23 16:35:43 +01:00
the-djmaze
91595ef6d5 Resolve ##1403 2024-01-23 16:35:22 +01:00
the-djmaze
4b8ad326fa Cleanup MIME header email address parsing 2024-01-23 14:35:41 +01:00
the-djmaze
443a6df070 Small change to show why saving message to Sent mailbox failed #1397 2024-01-23 14:34:41 +01:00
the-djmaze
29f6b2b743 Move Autocrypt parsing to JavaScript 2024-01-23 10:38:54 +01:00
the-djmaze
b726400f42 Resolve #1402 2024-01-23 10:23:06 +01:00
the-djmaze
5e35e39012 Bugfix: ask/send readReceipt was broken 2024-01-23 10:20:54 +01:00
the-djmaze
61c533a1a6 v2.33.0 2024-01-22 15:11:44 +01:00
the-djmaze
5e012bd66e Bugfix: redesign of addPgpEncrypted() failed 2024-01-22 14:56:05 +01:00
the-djmaze
a80cb495d2 Resolve #1396 2024-01-22 14:34:35 +01:00
the-djmaze
aca3490331 Resolve #1395 and #1394 2024-01-22 14:30:53 +01:00
the-djmaze
69d92ee184 workaround with issue to detect dkim 2024-01-22 14:03:09 +01:00
the-djmaze
f0bb3a8a45 Handle message hasVirus header in JavaScript 2024-01-22 13:33:00 +01:00
the-djmaze
e133cf5ffb Changes for BIMI-Selector #1394 2024-01-22 13:25:04 +01:00
the-djmaze
b7bab3cf21 bugfix: undefined MimePart 2024-01-22 11:07:21 +01:00
the-djmaze
9558456bcf Changes for #1397 2024-01-22 11:07:07 +01:00
the-djmaze
8749d189df cleanup mime header encoding handling 2024-01-22 09:19:45 +01:00
the-djmaze
4d9fb41d73 Cleanup mime header parameter handling 2024-01-22 09:03:34 +01:00
the-djmaze
5747b7add6 Version bump for #1400 2024-01-22 00:05:06 +01:00
the-djmaze
dac5b105ce
Merge pull request #1400 from PhilippMundhenk/master
Automatic login for ProxyAuth plugin, improved security documentation
2024-01-22 00:02:52 +01:00
the-djmaze
58955f792a Changes for BIMI-Selector #1394 2024-01-22 00:01:46 +01:00
the-djmaze
8292c47f7c Return all fetched messages headers in JSON.
This is in preperation of BIMI-Selectors #1394
2024-01-21 23:35:32 +01:00
Philipp Mundhenk
b947db2eb1 added documentation for usage of custom_logout_link 2024-01-21 15:31:32 +01:00
Philipp Mundhenk
59c0f5cc94 documented auto login, improved security documentation 2024-01-21 15:09:31 +01:00
Philipp Mundhenk
081726d764 added automatic login 2024-01-21 14:57:47 +01:00
the-djmaze
55a5fb491a Change SMTP sendRequestWithCheck for future support of RFC's 2024-01-18 03:19:03 +01:00
the-djmaze
48c3d655b4 Merge code to generate MIME PGP parts and MIME Plain parts 2024-01-18 03:17:27 +01:00
the-djmaze
ad985f94e5 Change a weird translation. This should be better 2024-01-17 01:59:44 +01:00
the-djmaze
7cf5edc74a Alsp check global AddressBook.sqlite on ClearUserData() 2024-01-17 01:58:49 +01:00
the-djmaze
b1c435277c Resolve #1393 2024-01-17 01:34:25 +01:00
the-djmaze
2d829178f8 Added feature to use the SQLite AddressBook per login account instead of global.
On by default unless the global AddressBook.sqlite already exists.
2024-01-17 00:55:23 +01:00
the-djmaze
b89a1b309c Bugfix: dataBaseUpgrade always runs on sqlite and pgsql
And split the db schema data into seperate class
2024-01-17 00:12:01 +01:00
the-djmaze
e965d58de0 Resolve #1140 2024-01-16 16:08:22 +01:00
the-djmaze
8f9c199360 Bump Nextcloud extension version 2024-01-16 15:49:15 +01:00
the-djmaze
816a2745bc Resolve #1392 2024-01-16 15:48:47 +01:00
the-djmaze
34e4bad77f Workaround #1392 2024-01-16 15:38:27 +01:00
the-djmaze
e4f3df3bb9
Merge pull request #1390 from PhilippMundhenk/master
implemented ProxyAuth
2024-01-16 01:36:45 +01:00
Philipp Mundhenk
f6eab84d40 added README 2024-01-14 18:22:59 +01:00
Philipp Mundhenk
84f739c72f added encryption 2024-01-14 18:22:41 +01:00
Philipp Mundhenk
ec937a5af1 implemented ProxyAuth 2024-01-14 14:39:23 +01:00
the-djmaze
30ed8dde11 Resolve #1381 2024-01-12 01:56:12 +01:00
the-djmaze
06fdf66748 Resolve #1379 2024-01-12 01:42:00 +01:00
the-djmaze
6b3f76e4ee Login form use method="POST" to prevent uri exposure when javascript fails 2024-01-09 15:56:35 +01:00
the-djmaze
2a3cea63cc KnockoutJS drop unused rateLimit method 2024-01-09 14:52:05 +01:00
the-djmaze
6b3c073ddd Don't watch-js 2024-01-09 01:28:34 +01:00
the-djmaze
c651112cc0 Cleanup some data-bind="" 2024-01-09 01:27:52 +01:00
the-djmaze
f7f3b0d373 Drop the disabled Knockout twoWayBindings 2024-01-08 18:33:37 +01:00
the-djmaze
c9f14318ea Bugfix: knockout title:value was removed, use attr:{title:value} 2024-01-08 18:32:15 +01:00
the-djmaze
e55938b653 Drop support for _ko_property_writers and for two-way binding they must be observables 2024-01-08 05:25:48 +01:00
the-djmaze
81ccee1152 Remove some unused Knockout code 2024-01-08 05:24:15 +01:00
the-djmaze
6a715da1ac Bugfix Sieve rfc5429 2024-01-08 04:55:18 +01:00
the-djmaze
a96ff94c6f Bugfix handling of RainLoop Sieve script 2024-01-03 02:41:42 +01:00
the-djmaze
f3935b1df4 Fix some Sieve parser issues 2024-01-03 01:47:52 +01:00
the-djmaze
62639acc5a Cleanup Sieve RainLoop filter editor 2024-01-03 01:31:44 +01:00
the-djmaze
42bcc0e047 Snappy New Year! 2024-01-02 17:44:34 +01:00
the-djmaze
9ebfa94fec docker hub php83 fails, trying php82 2023-12-31 13:33:31 +01:00
the-djmaze
f96bb39204 Docker hub use alpine linux 3.18.5 2023-12-31 13:29:26 +01:00
the-djmaze
f1350c83b7 docker hub to PHP 8.3 2023-12-31 13:25:06 +01:00
the-djmaze
55020137a9 Improved setup.php error log 2023-12-31 13:09:36 +01:00
the-djmaze
4a2de54c3e Changed some InvalidArgumentException to the better suited ValueError 2023-12-31 13:09:14 +01:00
the-djmaze
4d66c02190 Improve fix for #1373 2023-12-28 01:28:22 +01:00
the-djmaze
0a8dfe7d51 Resolve #1373 2023-12-28 01:23:56 +01:00
the-djmaze
38ef44e1d0 Added github sponsor link to Admin About page 2023-12-27 00:28:54 +01:00
the-djmaze
2467800247 Update funding choices 2023-12-26 23:56:23 +01:00
the-djmaze
b2477f2ce3 v2.32.0 2023-12-26 17:42:56 +01:00
the-djmaze
167c27c1ee Default IMAP message_list_limit to 10000 2023-12-26 17:20:27 +01:00
the-djmaze
b88794b54b Bugfix: Folders array_filter(): Argument #1 ($array) must be of type array, null given 2023-12-26 16:54:29 +01:00
the-djmaze
1a75a624cb Run full UID fetch in background when message_list_limit is set 2023-12-26 16:45:48 +01:00
the-djmaze
ef850e37f7 Cleanup SequenceSet 2023-12-26 16:26:12 +01:00
the-djmaze
501b0daa09 Make MessageListThreadsMap a background task.
Related to #1211
2023-12-26 02:03:49 +01:00
the-djmaze
7baa7af929 DoMessageCopy() return toFolder hash/etag 2023-12-26 00:59:30 +01:00
the-djmaze
508f5cc6e5 Improve Squire with isBrElement() 2023-12-26 00:58:43 +01:00
the-djmaze
98b203a3ca Demo plugin disable delete of messages 2023-12-25 16:07:59 +01:00
the-djmaze
d21b7e754f Cleanup, fix and improve some Squire handling 2023-12-25 16:07:42 +01:00
the-djmaze
4606ddf182 Change for #1360 2023-12-17 23:58:32 +01:00
the-djmaze
1a331dc3ba Some fixes for #1362 2023-12-17 22:42:48 +01:00
the-djmaze
4400af04c7
Merge pull request #1362 from Murena-SAS/dev/lang-fix-nexcloud
make nextcloud language work with snappy
2023-12-17 22:34:02 +01:00
the-djmaze
adaac297f9
Merge pull request #1363 from hampoelz/nc-theme-fixes
Nextcloud Theme fixes and improvements
2023-12-17 22:32:00 +01:00
hampoelz
e67776adf4 fix: attachment list simple 2023-12-17 14:02:03 +00:00
hampoelz
aae22db190 improve: message-list toolbar alignment 2023-12-17 12:36:45 +00:00
hampoelz
64c9d1cd40 fix: Nextcloud calendar picker 2023-12-17 12:04:46 +00:00
hampoelz
d260d97136 fix: Nextcloud file picker in Firefox 2023-12-17 11:54:33 +00:00
hampoelz
6bffd8e859 fix: first and last tabs label margin 2023-12-17 11:37:00 +00:00
hampoelz
2e892ff083 fix: login screen textbox padding 2023-12-17 11:35:39 +00:00
hampoelz
4ad603eeb1 fix: checkboxes in openpgp generate popup 2023-12-17 11:34:05 +00:00
hampoelz
a8647cbcae fix: remove checkbox outline on focus 2023-12-17 11:32:26 +00:00
hampoelz
4521f07acf fix: email addresses input dark theme 2023-12-17 11:28:43 +00:00
the-djmaze
dd46434895 Resolve #1361 2023-12-14 00:08:41 +01:00
Avinash Gusain
0a4f83f507 use getLanguages 2023-12-13 16:28:25 +05:30
Avinash Gusain
d1e6ea379e use getLanguages 2023-12-13 16:07:44 +05:30
Avinash Gusain
3a46916ad9 set default lang 2023-12-13 15:39:53 +05:30
Avinash Gusain
184d3fe7d6 nextcloud snapppy lang fix 2023-12-13 10:26:03 +05:30
the-djmaze
1d652e0ce3 Backup plugin don't use tar stream yet (was a test) 2023-12-12 22:55:02 +01:00
the-djmaze
f1c6ff3f2a Backup plugin use tar stream 2023-12-12 22:52:31 +01:00
the-djmaze
d1bab197c1 Backup using own TAR stream when PharData not available 2023-12-12 22:51:57 +01:00
the-djmaze
927c4e6ed1 Properly set CACHEDIR.TAG from http://www.brynosaurus.com/cachedir/ 2023-12-12 21:44:16 +01:00
the-djmaze
9fbe7e8d13 Draft rfc4467 for #288 2023-12-12 14:44:19 +01:00
the-djmaze
e0fa23822c Set static and themes folder to 0755 2023-12-12 14:11:53 +01:00
the-djmaze
c823ef6eae Rename messageVisibility to messageVisible 2023-12-12 14:11:25 +01:00
the-djmaze
7f0315dae4 Small indent fix 2023-12-12 01:08:04 +01:00
the-djmaze
2916583d0b Resolve #1332 2023-12-11 23:46:45 +01:00
the-djmaze
638755bef3 Theme NextcloudV25+ spacing to tabs 2023-12-11 23:08:14 +01:00
the-djmaze
2788ff3bcf Fix NExtcloudV25+ theme padding 2023-12-11 23:07:17 +01:00
the-djmaze
339ed3d44e Some dutch translations 2023-12-11 22:45:16 +01:00
the-djmaze
b4c6a3f18e Nextcloud use NextcloudV25+ theme by default 2023-12-11 22:45:00 +01:00
the-djmaze
c9d0a530c2 Provide Nextcloud dashboard widget code 2023-12-11 22:40:30 +01:00
the-djmaze
02366067c0 Fix upgrade issue 2023-12-11 16:46:50 +01:00
the-djmaze
f490c53919 Bugfix: Nextcloud FilterLanguage wrong param order #1352 2023-12-11 15:46:08 +01:00
the-djmaze
f47b77347d Small translate improvement 2023-12-11 13:50:36 +01:00
the-djmaze
8ce2a244e9 https://github.com/the-djmaze/snappymail/issues/1353#issuecomment-1849049228 2023-12-11 13:38:05 +01:00
the-djmaze
c3f7827683 All CSS font-size to % instead of px 2023-12-11 13:34:23 +01:00
the-djmaze
c1c851c238 Merge getNameAndEmailHelper() into newMessageCommand() 2023-12-11 11:50:02 +01:00
the-djmaze
08257f47cb Improved #1286 to be in line with all code 2023-12-11 11:38:38 +01:00
the-djmaze
65d469f71a
Merge pull request #1286 from rezaei92/master
Sending group email to all contact addresses
2023-12-11 11:07:30 +01:00
the-djmaze
bfb914132e Small change for #1350 2023-12-11 11:07:10 +01:00
the-djmaze
6f8abf33c8
Merge pull request #1350 from attike/attike-snappymail-plugins
New Plugin to handle multible identities: Use From-Address-Account for smtp
2023-12-10 17:05:07 +01:00
the-djmaze
4bb1e24222 Squire use double quotes as in original source 2023-12-10 17:04:01 +01:00
the-djmaze
c61f4a6918 Resolve #1042 2023-12-10 17:02:07 +01:00
the-djmaze
3db4827354 Removed title binding handler 2023-12-09 20:05:57 +01:00
the-djmaze
e3219a2636 Improved IMAP Capability detection 2023-12-09 19:37:28 +01:00
the-djmaze
ddc953f4fa v2.31.0 2023-12-08 22:24:04 +01:00
the-djmaze
7d45dce7ce Updated french language by @Cwpute 2023-12-08 21:57:07 +01:00
the-djmaze
91a505ea02 Resolve #1356 2023-12-08 21:48:03 +01:00
the-djmaze
9d7f93f304 Resolve #1354 2023-12-08 21:44:25 +01:00
the-djmaze
6d58b1efb5 Changed the squire code a bit 2023-12-08 01:33:27 +01:00
the-djmaze
e56415aa51 Different solution to use Nextcloud languages #1293 2023-12-08 01:28:08 +01:00
the-djmaze
8984726fd7 Added more language helpers 2023-12-08 01:09:58 +01:00
the-djmaze
66d9300b61 Cleaner language detection 2023-12-08 00:58:42 +01:00
attike
ca894d4427
Create README for new plugin 2023-12-07 00:59:18 +01:00
attike
051af6132e
new Plugin: SmtpUseFromAdrAccountPlugin 2023-12-07 00:57:08 +01:00
the-djmaze
a4724df69e
Merge pull request #1347 from SergeyMosin/reply-forward-indicators-fix
fix: flag indicators are added to wrong message when another message is selected while composing
2023-12-07 00:19:45 +01:00
the-djmaze
9675d03568 Resolve https://github.com/fastmail/Squire/issues/449 2023-12-07 00:07:31 +01:00
the-djmaze
f008f948d2 Fix some Squire editors to merge with v2.2.5 2023-12-07 00:05:01 +01:00
Sergey Mosin
853727541f fix: flag indicators are added to wrong message when another message is selected while composing 2023-12-05 14:08:31 -05:00
the-djmaze
fa15ff6ba4 Draft idea for custom notification sounds 2023-12-05 16:18:36 +01:00
the-djmaze
5d6fb0c705 Track the inUndoState 2023-12-05 16:18:17 +01:00
the-djmaze
7e2cc69a23 Resolve #1296 2023-12-05 15:00:09 +01:00
the-djmaze
e2149ea94c Sync some Squire code with main project code 2023-12-05 02:02:55 +01:00
the-djmaze
9198ce187b Changes for #1296
Can be tested at https://snappymail.eu/demo/
2023-12-04 17:45:57 +01:00
the-djmaze
039e2252de range.collapse(false) is default 2023-12-04 16:20:25 +01:00
the-djmaze
a5d749ec05 Squire cleanTree() use while instead of for 2023-12-04 16:19:28 +01:00
the-djmaze
280660314f Squire drop support for iPod 2023-12-04 15:47:18 +01:00
the-djmaze
58666c0df5 Resolve #1344 2023-12-04 13:47:45 +01:00
the-djmaze
28ee711938 Rearrange squire source code to better diff with originals 2023-12-04 13:31:47 +01:00
the-djmaze
7441b64de0 Bugfix: Nextcloud Call to undefined method RainLoop\\Model\\MainAccount::ImapConnectAndLoginHelper() 2023-12-04 03:39:51 +01:00
the-djmaze
cbfb83e4db Update Nextcloud screenshot 2023-12-04 02:39:45 +01:00
the-djmaze
2548f30a2a SnappyMail failed due to Nextcloud Symfony polyfill 2023-12-04 02:30:39 +01:00
the-djmaze
0c6895f7d7 v2.30.0 2023-12-04 02:03:57 +01:00
the-djmaze
95ec5e6bb0 Improve security of SensitiveString 2023-12-04 01:36:21 +01:00
the-djmaze
b7b266defc Merge branch 'feature/1343-secure-parameters' 2023-12-04 00:52:50 +01:00
the-djmaze
7bd76a19b5 Set more #[\SensitiveParameter] for #1343 2023-12-04 00:41:20 +01:00
the-djmaze
f869ed5121 More improvements for #1343 2023-12-04 00:25:26 +01:00
the-djmaze
870d78d5a3 Split PHP polyfills from include.php 2023-12-03 23:12:33 +01:00
the-djmaze
089c229213 Bugfix: account sSmtpPassword was wrong 2023-12-03 23:09:46 +01:00
the-djmaze
13f8ab5caf Improved the idea for #1343 2023-12-03 23:08:44 +01:00
the-djmaze
be70870b72 Split PHP polyfills from include.php 2023-12-03 22:09:56 +01:00
the-djmaze
cabc652aa2 Simple idea for #1343 2023-12-03 21:42:30 +01:00
the-djmaze
c61c3c6ce7 Revert #1343
PHP Fatal error: Attribute "SensitiveParameter" cannot target property
2023-12-03 21:34:29 +01:00
the-djmaze
84169179bc
Merge pull request #1343 from edlerd/patch-1
Mark Account password as sensitive information
2023-12-03 21:20:17 +01:00
the-djmaze
09bd6d5335 Disable snappymail/v/0.0.0/static/.htaccess for now as many servers have issues with it 2023-12-03 21:14:20 +01:00
David Edler
758ce9805e
Mark Account password as sensitive information, so it won't get included in log files
see https://www.php.net/manual/en/class.sensitiveparameter.php
2023-12-03 20:57:29 +01:00
the-djmaze
20058edd71 dragData already set by setDragAction 2023-12-03 20:40:54 +01:00
the-djmaze
ef5c3f73a5 Don't disable STARTTLS 2023-12-03 17:57:49 +01:00
the-djmaze
2400c6def8 Use SASL-IR for #1245 2023-12-03 17:57:27 +01:00
the-djmaze
56a5f29f26 Also allow disable imap login capabilities 2023-12-03 17:44:26 +01:00
the-djmaze
465d7ed673 Admin -> Domain after login test show all capabilities to disable 2023-12-03 17:36:41 +01:00
the-djmaze
cf910c678b Revamp Domains Settings IMAP disabled capabilities to extend it 2023-12-03 17:08:25 +01:00
the-djmaze
e6b2cdfcdf Merge branch 'master' of github.com:the-djmaze/snappymail 2023-12-03 12:59:13 +01:00
the-djmaze
b5552c1408 Resolve #1245 2023-12-03 12:58:54 +01:00
the-djmaze
a5da1eee65
Merge pull request #1341 from hguilbert/patch-12
Update FR user.json
2023-12-03 12:25:08 +01:00
hguilbert
67e1e6ab05
Update FR user.json 2023-12-02 07:12:20 +01:00
the-djmaze
57c000908f Resolve #951 2023-12-01 23:18:08 +01:00
the-djmaze
7e30c50dfe Merge viewPopupMessage() into popupMessage() 2023-12-01 23:00:39 +01:00
the-djmaze
004de4e48c Added some CSP info 2023-12-01 22:59:36 +01:00
the-djmaze
b0fea1e7c1 Hint for #1177 2023-12-01 01:28:05 +01:00
the-djmaze
f8d990a25f Resolve #1339 2023-12-01 01:15:19 +01:00
the-djmaze
e9ad4e38c2 Fix plugin admin permission 2023-11-30 00:10:16 +01:00
the-djmaze
db629bf29c Small improvement for #1338 2023-11-29 23:07:50 +01:00
the-djmaze
fb8d557099 Resolve #1338 2023-11-29 22:45:40 +01:00
the-djmaze
d75f64904f Bugfix: Creation of dynamic property SnappyMail\Stream\ZipEntry::$compression is deprecated 2023-11-29 22:17:30 +01:00
the-djmaze
b05e4f609c Drop unused ImapClient::IsSupported() 2023-11-29 22:00:58 +01:00
the-djmaze
56a0fd63ce Drop obsolete $_ENV['SNAPPYMAIL_NEXTCLOUD'] 2023-11-28 18:40:01 +01:00
the-djmaze
3db4ff2a82 Some code for Nextcloud "unread mail" widet #569 2023-11-28 18:39:42 +01:00
the-djmaze
c1017499e4 Improved example plugin to test for admin section 2023-11-28 10:25:36 +01:00
the-djmaze
603d48f739 Make addJsonHook() $sFunctionName param optional 2023-11-28 10:25:13 +01:00
the-djmaze
44ddb2d78e Remove unused Plugin->replaceTemplate 2023-11-28 09:40:39 +01:00
the-djmaze
5412348a65 Example code for #1042 2023-11-27 18:06:12 +01:00
the-djmaze
5effa242ae Improved Zip::addRecursive 2023-11-27 18:05:00 +01:00
the-djmaze
086f5ebb06 Bugfix: handling services images failed 2023-11-27 17:05:30 +01:00
the-djmaze
fdc21f26fc Bugfix: json.after-* hook didn't send $aResponse as recursive array 2023-11-27 17:04:36 +01:00
the-djmaze
daf1e2769f Resolve #1289 2023-11-27 15:07:02 +01:00
the-djmaze
771c6f7559 Idea to fetch all UID's in background 2023-11-27 14:36:46 +01:00
the-djmaze
612aa09071 Enable thread view in demo #445 2023-11-27 02:33:43 +01:00
the-djmaze
7135af995d Extend example extension code with more details 2023-11-27 01:51:23 +01:00
the-djmaze
5e2d9c4fb0 set dropdown menu initDom to null 2023-11-26 18:06:42 +01:00
the-djmaze
77d2d099e2 Resolve Add a "Move to" button inside message view #1295 2023-11-26 18:06:19 +01:00
the-djmaze
eb802135a9 Add folder full name to message uids set 2023-11-26 17:52:20 +01:00
the-djmaze
df7888e3a6 Remove the openDropdownTrigger and replace some with a better toggle 2023-11-26 17:50:18 +01:00
the-djmaze
9d876d1146 Draft code: on message view set hash 2023-11-26 15:11:01 +01:00
the-djmaze
9e124f0572 IMAP ENABLE must be supported? 2023-11-26 14:48:21 +01:00
the-djmaze
787907f42a Define RFC 8970 PREVIEW 2023-11-26 03:04:34 +01:00
the-djmaze
00302d2064
Merge pull request #1336 from leojonathanoh/enhancement/docker-add-more-php-extensions
Enhancement (docker): Add more php extensions
2023-11-26 02:57:32 +01:00
the-djmaze
85c5168cb6
Merge pull request #1335 from leojonathanoh/fix/development-fix-self-signed-certs-verification-in-docker-compose.yml
Fix (development): Fix self-signed certs verification in `docker-compose.yml`
2023-11-26 02:57:24 +01:00
the-djmaze
a2b638dc55 Remove all String.trim() from Sieve RainLoop script #1329 2023-11-26 02:55:08 +01:00
the-djmaze
bcb6d30023 More in line with Squire v2.2.5 2023-11-26 02:47:40 +01:00
the-djmaze
0423c50c51 Resolve #1337 2023-11-26 02:30:08 +01:00
the-djmaze
fc0074dfd2 Squire: unused root 2023-11-26 02:29:48 +01:00
the-djmaze
1db1bdf675 Squire: Remove redundant <span> after replacing styles
e4ae7c2b81
2023-11-26 02:20:26 +01:00
the-djmaze
beb2c7f577 Squire: Handle empty nodes in moveRangeBoundariesDownTree
b85754ca50
2023-11-26 02:16:15 +01:00
Leonard Jonathan Oh
b58006503c Enhancement (docker): Add more php extensions 2023-11-24 12:31:31 +00:00
Leonard Jonathan Oh
25bd3d441e Fix (development): Fix self-signed certs verification in docker-compose.yml
This allows `Require verification of SSL certificate` to work as expected.
2023-11-24 10:47:32 +00:00
the-djmaze
677e1863e5 Resolve #1331 2023-11-24 00:44:11 +01:00
the-djmaze
50f73dde75
Merge pull request #1334 from leojonathanoh/enhancement/ci-create-separate-workflow-for-docker-pr-builds
Enhancement (ci): Use separate workflow for PR docker builds
2023-11-24 00:22:57 +01:00
the-djmaze
eb6ac32798
Merge pull request #1327 from leojonathanoh/enhancement/development-update-docker-compose.yml-with-working-stack
Enhancement (development): Update `docker-compose.yml` with working stack
2023-11-24 00:21:37 +01:00
the-djmaze
23d1eb0e28
Merge pull request #1330 from cm-schl/master
Update German Translation
2023-11-24 00:20:28 +01:00
the-djmaze
5047c3c6b8 Resolve #1328 2023-11-24 00:16:08 +01:00
Leonard Jonathan Oh
7183e2614e Enhancement (ci): Use separate workflow for PR docker builds
This is so the `docker` workflow status is not polluted with PR builds.
2023-11-23 22:44:01 +00:00
Leonard Jonathan Oh
1ad64e9fc9 Enhancement (development): Update docker-compose.yml with working stack
This is for those who want to try out the full stack in development.

```sh
gulp  # Generate static assets
docker-compose up --build
```
2023-11-23 21:57:43 +00:00
leo
ee99590a63
Enhancement (docker): Add more archs (#1321)
* Enhancement (docker): Add `linux/arm64` arch

* Fix
2023-11-23 20:21:47 +01:00
cm-schl
aabce7c951
Update admin.json 2023-11-23 16:34:41 +01:00
the-djmaze
bde5060008 Revert #1323
As rfc5228 implementations MUST support the "keep", "discard", and "redirect" actions.
2023-11-23 01:46:46 +01:00
the-djmaze
0736782a44 Resolve #1323 2023-11-23 01:34:55 +01:00
the-djmaze
7dc4002fb4 Bugfix: loading integrity.php failed 2023-11-23 00:48:20 +01:00
leo
60700a56e9
Fix (docker): Use 127.0.0.1 instead of localhost to test snappymail in entrypoint.sh (#1325)
* Fix (docker): Use `127.0.0.1` instead of `localhost` to test snappymail in `entrypoint.sh`

* Run build on PR but don't push

* Login only on tags

* Fix test
2023-11-23 00:36:36 +01:00
the-djmaze
60964c9869 Activate OLDER and YOUNGER of RFC 5032 2023-11-23 00:35:52 +01:00
the-djmaze
ad3d8c87ec Revert "Fix: listen on IPv6 socket as well in container (#1324)"
This reverts commit 1e597f9cd7.
2023-11-23 00:31:32 +01:00
the-djmaze
6f2f89c365 Add support for IMAP WITHIN https://datatracker.ietf.org/doc/html/rfc5032 2023-11-23 00:11:30 +01:00
Andriy Kushnir
1e597f9cd7
Fix: listen on IPv6 socket as well in container (#1324) 2023-11-22 23:25:18 +01:00
the-djmaze
79348f975f Added some IMAP connection examples 2023-11-22 18:18:29 +01:00
the-djmaze
cb2f641378 Put domain capabilities in ?admin domain test response 2023-11-22 17:45:24 +01:00
the-djmaze
0dfcc52728 No need to check for libxml_use_internal_errors 2023-11-22 12:52:56 +01:00
the-djmaze
288eedf6a1 Improved test output with colors 2023-11-22 12:52:43 +01:00
the-djmaze
3bff3555a0 Added a test due to Failed loading libs.min.js #358, #862, #890, #895, #1238, #1320 2023-11-22 12:47:14 +01:00
the-djmaze
4f5db79e27 Activate RFC 4314 MYRIGHTS 2023-11-22 04:50:01 +01:00
the-djmaze
4c0b0b102a Prioritize LIST-EXTENDED over LSUB (LSUB deprecated in IMAP4rev2) 2023-11-22 04:09:26 +01:00
the-djmaze
5cdbba8da0 Support \noinferiors to disallow creating subfolders 2023-11-22 04:07:57 +01:00
the-djmaze
d74407f36b Resolve #1318 2023-11-22 00:13:22 +01:00
the-djmaze
40a352c3c0 No {{sha}} in docker image names? 2023-11-22 00:08:45 +01:00
the-djmaze
9e2d8d02b9 Update changelog 2023-11-21 23:54:42 +01:00
the-djmaze
9ee2f46a99 v2.29.4 2023-11-21 23:49:16 +01:00
the-djmaze
a0eac266f7 Resolve #1319 2023-11-21 23:47:35 +01:00
the-djmaze
aa9888d2df Drop application.ini for Docker #965 2023-11-21 21:10:33 +01:00
the-djmaze
b77bf0745e Update README with correct docker workflow images and urls 2023-11-21 20:53:28 +01:00
the-djmaze
e10bc516fe v2.29.3 2023-11-21 16:01:47 +01:00
the-djmaze
f6764b3616 Resolve #1284 2023-11-21 14:19:54 +01:00
the-djmaze
50fef1cf69 small code cleanup 2023-11-21 13:56:16 +01:00
the-djmaze
9fcf2658d0 docker hub is djmaze/snappymail 2023-11-21 13:41:41 +01:00
the-djmaze
81a1b12005 Only build docker images on release tags 2023-11-21 13:12:44 +01:00
the-djmaze
366ef575f1 Remove TODO from docker workflow 2023-11-21 12:56:21 +01:00
the-djmaze
9ded40ba9c Idea to prevent exit inside Nextcloud 2023-11-21 00:13:22 +01:00
the-djmaze
8247357184 Some changes for #1293 2023-11-20 22:40:33 +01:00
the-djmaze
930be49775 Resolve #1008 2023-11-20 15:50:03 +01:00
leo
24dbff999e
Enhancement: Add Docker Hub image (#965)
* Enhancement: Add Docker Hub image

* Push to ghcr.io

* Add all `org.opencontainers.image.xxx` labels using `--label` instead of `LABEL`

* Change `php-fpm` to listen on `:9000` to expose prometheus metrics via `php-fpm_exporter`

* Include `yarn.lock` to speed up builds

* Reorder

* Use correct sha for pr

* Add logging before overriding values in config

* Add `DEBUG=true` env var for verbose `entrypoint.sh` logs

* Print php exception when snappymail fails to generate data directory and config file on the very first time

* Log snappymail version

* Fix COPY statement

* Add .dockerignore

* Add `USER` and `ENTRYPOINT`

* Update `.dockerignore`

* Add docker image test

* Push only if image test succeeds

* Log when startup is successful

* Use plain `docker build` in `build_and_test.sh`

* Fix test output
2023-11-20 15:01:26 +01:00
the-djmaze
6552a93989 Changes for #929 2023-11-20 12:50:47 +01:00
the-djmaze
87381e2e4d Resolve #1302 2023-11-17 00:37:21 +01:00
the-djmaze
36c8b97cb1 Merge branch 'master' of github.com:the-djmaze/snappymail 2023-11-17 00:09:00 +01:00
the-djmaze
e918889c3b Resolve #1306 and #1313 due to localStorage failure 2023-11-17 00:08:32 +01:00
the-djmaze
e7d7ca48e4
Merge pull request #1301 from SergeyMosin/mailbox-uri-fix
Fix: '#/mailbox/folder/mUID/search' uri/route handling
2023-11-15 00:42:49 +01:00
the-djmaze
c8baabf8d9 Sabre/Xml 4.0.4 for #1311 2023-11-14 20:41:46 +01:00
the-djmaze
70a2ebb671 Sabre/VObject 4.5.4 for #1311 2023-11-14 20:25:39 +01:00
the-djmaze
90a657c0fb Merge branch 'master' of github.com:the-djmaze/snappymail 2023-11-14 17:13:01 +01:00
the-djmaze
2611a2e067 v2.29.2 2023-11-14 17:12:42 +01:00
the-djmaze
442c4f5eb5
Merge pull request #1304 from tkasch/reply-fix
Fix threading view in Thunderbird (others?)
2023-11-14 17:05:00 +01:00
the-djmaze
85b4e23d31
Merge pull request #1307 from robertoszek/master
add video-on-login-screen plugin
2023-11-14 17:02:03 +01:00
the-djmaze
13c23141ee Resolve #1303
And STATUS SIZE is part of RFC 8438, not RFC 9051
2023-11-14 16:49:38 +01:00
the-djmaze
9bdd7d5564 Define sortMode inside addObservablesTo() 2023-11-14 11:28:50 +01:00
the-djmaze
9bb2399e47 Resolve #1308 2023-11-14 11:14:40 +01:00
robertoszek
b34d87a209
add video-on-login-screen plugin 2023-11-09 21:24:48 +01:00
Torsten Kasch
cdd74c8f20 Fix threading view in Thunderbird (others?)
Replies sent via SnappyMail break thread view in Thunderbird
due to not standard-compliant Reply-To and References headers.

* Due to the long IDs, the headers `Reply-To`, `References` (and `Messge-ID`)
  are split between the header name and its value. According to section RFC5322,
  section 2.2.3, this may be legal but it obviously causes problems.

* On subsequent replies, the parent's ID gets prepended to the `References`
  header. Section 3.6.4 of the same RFC specifies that the ID should be
  appended to that header.
2023-11-08 16:35:46 +01:00
Sergey Mosin
0bd3545a34 improve: nextcloud iframe mode unified search result click handling 2023-11-07 09:34:26 -05:00
Sergey Mosin
4b26317be8 fix: '#/mailbox/folder/mUID/search' URI does not show the message 2023-11-07 09:34:21 -05:00
the-djmaze
09e3c7bec6 Rename FolderUserStore.folderListError to FolderUserStore.error 2023-10-31 01:39:40 +01:00
the-djmaze
f097c8bb92 Rename folderListOptimized to optimized 2023-10-31 01:38:09 +01:00
the-djmaze
1a5ff4772d Drop unused AccountUserStore.signature
Drop debounce from AccountUserStore.loading
2023-10-31 01:36:41 +01:00
Mohammad Rezaei
c402d90256 Improve the group mail features styles 2023-10-29 11:05:33 +03:30
the-djmaze
36789bd3b5 Cleanup knoin 2023-10-24 17:25:09 +02:00
the-djmaze
e6a60b879e No need for __dom property 2023-10-24 17:07:14 +02:00
the-djmaze
bd737a82bf Improve solution for #1241 2023-10-24 11:43:47 +02:00
the-djmaze
0064a44031 https://github.com/the-djmaze/snappymail/pull/629#issuecomment-1752025230 2023-10-24 00:56:29 +02:00
the-djmaze
adc300d636 Resolve #1241 2023-10-24 00:51:32 +02:00
the-djmaze
1bf252ad7e Revert #1241 due to #1283 2023-10-24 00:42:04 +02:00
Mohammad Rezaei
73dd4677da Sending group email to all contact addresses 2023-10-16 15:41:28 +03:30
the-djmaze
9a4b73e819 Resolve #1270 2023-10-08 14:59:03 +02:00
the-djmaze
8fda2f4955 No need to check for !oMoveFolder && !bDelete 2023-10-08 14:45:50 +02:00
the-djmaze
bbf45ea979 Resolve #1274 2023-10-08 14:45:20 +02:00
the-djmaze
b3309f7b4e Cleanup messagelist UpOrDown handling 2023-10-08 14:07:02 +02:00
the-djmaze
59f9ccdbdc Remove messagelist UpOrDown delay 2023-10-08 14:03:32 +02:00
the-djmaze
231ccb1e43 Cleanup and simplify messageList UpOrDown handling 2023-10-08 13:57:46 +02:00
the-djmaze
8841464f84 Remove unused returnValue 2023-10-08 12:30:38 +02:00
the-djmaze
04e2869953 Nextcloud plugin v2.25 2023-10-08 12:30:25 +02:00
the-djmaze
845d1d9fb3
Merge pull request #1263 from mayswind/master
update Chinese translation
2023-10-04 01:18:05 +02:00
the-djmaze
94f64a6e3f
Merge pull request #1266 from theronakpatel/createCalendarListItem-class-reposition
Create calendar list item class reposition
2023-10-03 21:09:01 +02:00
Ronak Patel
f0d90db640 Change of class position createCalendarListItem 2023-10-03 09:49:03 -07:00
Ronak Patel
5200917e0a Change of class position createCalendarListItem 2023-10-03 09:48:22 -07:00
Ronak Patel
8e485c3cab Change of class position createCalendarListItem 2023-10-03 09:47:57 -07:00
Ronak Patel
019a5024f9 Change of class position createCalendarListItem 2023-10-03 09:46:03 -07:00
MaysWind
ad17df8653 update Chinese translation 2023-10-03 20:58:46 +08:00
the-djmaze
9b26d0f60a v2.29.1 2023-10-02 18:42:13 +02:00
the-djmaze
e1649d0361 Drop .actionHandle 2023-10-02 18:35:59 +02:00
the-djmaze
57b804f3a3 Bugfix: using messages list checkboxes failed 2023-10-02 18:34:33 +02:00
the-djmaze
396caabdaa Bugfix: flagParent failed due to my change for #1248 2023-10-02 18:05:12 +02:00
the-djmaze
a25350aa60 v2.29.0 2023-10-02 17:05:42 +02:00
the-djmaze
d4e76fb42c Improved solution for #1241 2023-10-02 16:36:22 +02:00
the-djmaze
edad4ac025 Resolve #1241 2023-10-02 16:15:29 +02:00
the-djmaze
26b7abf893 Other solutions for nextcloud auto login #1247 due to a TOTP conflict 2023-10-02 15:51:19 +02:00
the-djmaze
31d50cf67e Remove spacing 2023-10-02 15:38:02 +02:00
the-djmaze
d0e6a78f72 Bump version #1251 2023-10-02 15:22:24 +02:00
the-djmaze
611c5f2631 Resolve #1250 2023-10-02 14:46:26 +02:00
the-djmaze
1d27c22085 Resolve #1251 2023-10-02 14:41:27 +02:00
the-djmaze
6b0e6ec02a Improved selector changes of #1248 2023-10-02 12:47:47 +02:00
the-djmaze
63d0d138e6 Simplify change of #1247 2023-10-02 12:26:34 +02:00
the-djmaze
7a49d55736
Merge pull request #1248 from SergeyMosin/some-fixes
Some fixes
2023-10-01 11:46:05 +02:00
the-djmaze
775faa9d79 #1243 version bump 2023-10-01 11:18:37 +02:00
the-djmaze
ebf16393fc
Merge pull request #1243 from Alphix/patch-1
[ldap-contacts-suggestions] Shorten overlong label
2023-10-01 11:15:48 +02:00
the-djmaze
4cc1e8429e
Merge pull request #1247 from SergeyMosin/some-improvements
Some improvements
2023-10-01 11:13:55 +02:00
the-djmaze
c41e95dc3b
Merge pull request #1259 from theronakpatel/add-to-calendar-redesign
Improve UX of "Put in Calendar" option in nextcloud plugin
2023-10-01 11:05:18 +02:00
Ronak Patel
c599847aea Make the Put in calendar option more obvious in Mail 2023-09-28 11:06:21 -07:00
Sergey Mosin
79c8281a6b improve: nextcloud: 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. 2023-09-18 15:54:19 -04:00
Sergey Mosin
35cd354353 improve: add 'collapse_blockquotes', 'allow_spellcheck' and 'mail_list_grouped' to admin settings ('defaults' section) 2023-09-18 15:54:14 -04:00
Sergey Mosin
22691450af improve: if applicable set '\\answered' or '$forwarded' flag after a message is sent so the proper icon is shown in the message list view 2023-09-18 15:54:14 -04:00
Sergey Mosin
bafd17d029 improve: spellcheck the subject when 'allowSpellcheck' setting is true 2023-09-18 15:54:14 -04:00
Sergey Mosin
a8710bacc9 improve: in mobile mode hide folders(left) panel when a folder is clicked 2023-09-18 15:54:14 -04:00
Sergey Mosin
dadb6b86c3 improve: address input: space character can trigger '_parseValue' if the email address looks complete 2023-09-18 15:54:14 -04:00
Sergey Mosin
d97ccf0cfb improve: add "Add/Edit signature" label to PopupsIdentity.html 2023-09-18 15:54:14 -04:00
Sergey Mosin
11c616715d improve: use calendar icon in message list for messages with '.ics' or 'text/calendar' attachments 2023-09-18 15:54:14 -04:00
Sergey Mosin
8ab3055223 fix: unseen indicator is not shown in thread view when 'listGrouped' settings is false + show unseen message count when the message list is threaded 2023-09-18 15:52:39 -04:00
Sergey Mosin
1b7e144a6e fix: <font> tag 'style' is lost in replies 2023-09-18 15:50:49 -04:00
Sergey Mosin
d5965bf1d7 fix: nextcloud iFrame mode: click on unified search result opens inner iFrame 2023-09-18 15:50:49 -04:00
Sergey Mosin
15e59ef65f fix: allow clicking action on entire "ListItem" area, instead of just the "text elements" 2023-09-18 15:50:48 -04:00
David Härdeman
a221ebfab8
[ldap-contacts-suggestions] Shorten overlong label
This is just cosmetic, but the "to use" is superfluous and doesn't match the wording of the other labels (and makes the rendered UI a bit weird by pushing everything to the side).
2023-09-13 01:07:45 +02:00
the-djmaze
49010a70a0 Resolve #1237 2023-09-11 21:44:57 +02:00
the-djmaze
c60f8776f4 Resolve #1208 2023-08-29 16:08:24 +02:00
the-djmaze
4f3265f8b6 Resolve #1215 2023-08-28 17:44:07 +02:00
the-djmaze
8b246e64a6 Added idea to use crypt seal in the future when login password changes 2023-08-28 16:59:00 +02:00
the-djmaze
335b6bc5c8 Added comment to public function GenerateFilePath() 2023-08-28 15:50:17 +02:00
the-djmaze
44c4514622 Allow to call setup.php outside to test PHP version and extensions #1209 2023-08-28 14:43:53 +02:00
the-djmaze
120873c7d6 Added "View ICS" extension #1178 2023-08-28 13:34:23 +02:00
the-djmaze
5251d85fef Changes for #1234 2023-08-28 12:05:56 +02:00
the-djmaze
6b42620ad3 Cleanup some squire code 2023-08-28 12:05:02 +02:00
the-djmaze
e9770e17f6
Merge pull request #1234 from SergeyMosin/text-nodes-in-squire-root
fix: unwrapped text nodes attached to squire._root
2023-08-28 11:02:16 +02:00
the-djmaze
eb566ca9db Changes for idea in #1150 (isSmall not active yet) 2023-08-28 10:11:34 +02:00
the-djmaze
53f8abc311 Bugfix: undefined call to parseStreamRecursive() 2023-08-28 09:16:15 +02:00
Sergey Mosin
626d52b15a fix: unwrapped text nodes attached to squire._root 2023-08-23 17:20:03 -04:00
the-djmaze
8f79a4c34b Show paperclip at EML attachments #1166 2023-08-22 13:57:36 +02:00
the-djmaze
ba304fcd00 Moved NextcloudV25+ theme to Nextcloud integration #629 2023-08-22 12:58:29 +02:00
the-djmaze
64bb993ffa
Merge pull request #629 from hampoelz/nc-theme
Modern UI / Nextcloud Theme
2023-08-22 12:53:58 +02:00
the-djmaze
ab986677dc Color picker use color blind palette "Tableau 10" by Maureen Stone by default #1199 2023-08-22 11:40:13 +02:00
the-djmaze
54344a5b54 Idea for #1199 2023-08-21 17:12:24 +02:00
the-djmaze
4e04461f80 Changes for "Request failed" error #1220 2023-08-21 16:02:57 +02:00
the-djmaze
4c5f19579c Fix Fetch controller.abort(reason) handling #1220 2023-08-21 11:31:57 +02:00
the-djmaze
a3bb108b33 Fix for previous commit regarding UTF-8 #1230 2023-08-21 10:03:36 +02:00
the-djmaze
037ed7e257 Change for #1225 2023-08-21 10:01:10 +02:00
the-djmaze
d6eb0d927a Resolve #1230 2023-08-21 09:12:27 +02:00
hampoelz
7fc268e518 fix: avatar size and alignment 2023-08-13 10:20:15 +00:00
the-djmaze
83456164c7 Resolve #1228 2023-08-08 09:59:53 +02:00
hampoelz
bbd100c544 chore: Add slight contrast to message list toolbar 2023-08-04 11:40:42 +00:00
Rene Hampölz
47ed699525
Merge branch 'the-djmaze:master' into nc-theme 2023-07-27 11:22:16 +02:00
hampoelz
0e0d8c8962 fix: Message flags default color in dropdown 2023-07-27 08:59:26 +00:00
hampoelz
a667b46316 chore: Rename sidebar menu item 2023-07-27 08:51:10 +00:00
hampoelz
af37b2b202 fix: Text overflow in sidebar menu items 2023-07-27 08:49:58 +00:00
the-djmaze
f6ef6d45ff
Merge pull request #1212 from SergeyMosin/squire-ui-font-info
Update editor font-family and font-size drop-downs
2023-07-24 15:42:08 +02:00
the-djmaze
a6c81bb071
Merge pull request #1207 from chiyi4488/patch-1
Update TC user.json
2023-07-24 15:41:58 +02:00
the-djmaze
2ccf758c8e
Merge pull request #1206 from MKjanek32/master
Improved Polish translation and Nextcloud internationalization
2023-07-24 15:41:50 +02:00
Sergey Mosin
1fcada1b07 squire ui: show current(under cursor) font-family and font-size in the editor toolbar 2023-07-14 14:16:15 -04:00
Yi Chi 齊一
08c5d6e7dd
Update user.json 2023-07-12 11:28:53 +08:00
the-djmaze
99d68bc354 Workaroung imagick improper image header #1201 2023-07-11 10:24:50 +02:00
the-djmaze
1eb192db72 v2.28.4 2023-07-10 14:27:21 +02:00
the-djmaze
76e82ba159 Resolve #1205 2023-07-10 11:01:46 +02:00
the-djmaze
c669a8e55f
Merge pull request #1203 from tinola/master
update version number
2023-07-10 10:34:37 +02:00
the-djmaze
024efd3be7
Merge pull request #1204 from SergeyMosin/more-default-configs
More default configs
2023-07-10 10:34:15 +02:00
Sergey Mosin
ceeb55acf2 improve: only show 'Add "domain.tld" as an application for mailto links?' message after login (firefox shows the message on every reload otherwise). 2023-07-09 09:06:20 -04:00
Sergey Mosin
c75c0e9ed1 fix: 'reloadTime' function result is passed into 'setInterval' instead of the function 2023-07-09 09:06:16 -04:00
Sergey Mosin
c59d19781f add 'view_images' to admin settings ('defaults' section) 2023-07-09 09:06:11 -04:00
Sergey Mosin
2e0077adec add 'view_show_next_message' to admin settings ('defaults' section) 2023-07-09 09:06:05 -04:00
Sergey Mosin
665aa4a226 add 'msg_default_action' to admin settings ('defaults' section) 2023-07-09 09:05:58 -04:00
tinola
231480e775 update version number 2023-07-07 17:13:44 +02:00
the-djmaze
19ea5de1bf Convert getPdoAccessData() : array to a RainLoop\Pdo\Settings object instance 2023-07-04 11:43:23 +02:00
the-djmaze
4cd24b87ff Translate for new BIDI #1200 2023-07-04 09:47:02 +02:00
the-djmaze
63f9137812
Merge pull request #1200 from rezaei92/master
Added bidi buttons to Squire editor
2023-07-04 09:36:25 +02:00
the-djmaze
f6348123db Resolve #1195 2023-07-03 15:33:38 +02:00
the-djmaze
eab3d8d938 Improved #1197 2023-07-03 15:16:52 +02:00
the-djmaze
2f746cb262
Merge pull request #1197 from SergeyMosin/nexctloud-custom-config
nextcloud: add ability to include custom php file in InstallStep migration
2023-07-03 15:10:55 +02:00
the-djmaze
825ba99a08 Change for #358 2023-07-03 11:51:32 +02:00
the-djmaze
0e3ed0fea0 Resolve #1192 2023-07-03 11:05:11 +02:00
Rene Hampölz
a2814e19c3 chore: Add fallback variables 2023-07-01 15:37:59 +00:00
Rene Hampölz
87215506fe refactor: Cleanup 2023-07-01 14:53:40 +00:00
Rene Hampölz
009428c72f fix: Side panel footer buttons in RTL layout 2023-07-01 14:19:09 +00:00
Mohammad rezaei
b2b07ab920
Added bidi button to Squire editor 2023-07-01 17:31:07 +03:30
Rene Hampölz
7c2e456ab0 revert: Tablet layout parts 2023-07-01 13:47:14 +00:00
Sergey Mosin
cb96ad95c4 nextcloud: add ability to include custom php file in InstallStep migration 2023-06-30 21:46:21 -04:00
Jan Potocki
b0ca61eff5 Updated Polish translation 2023-06-29 18:51:19 +02:00
Jan Potocki
c09f71e9a9 Updated Polish translation for Nextcloud integration 2023-06-29 18:50:29 +02:00
Jan Potocki
3d2c9836e2 Fixed internationalization for Nextcloud integration 2023-06-29 18:49:03 +02:00
the-djmaze
47e3fc90f2 Resolve #1186 2023-06-26 12:41:47 +02:00
the-djmaze
841fca17cd v2.28.3 2023-06-22 13:28:07 +02:00
the-djmaze
739a2ce39e Attachments in "new window" view #1166 2023-06-22 13:22:32 +02:00
the-djmaze
1a499f61fd Resolve #1187 2023-06-22 12:54:52 +02:00
the-djmaze
0ab66e175a Resolve #1182 2023-06-22 12:49:26 +02:00
the-djmaze
11a79da01a Merge branch 'master' of github.com:the-djmaze/snappymail 2023-06-22 12:43:12 +02:00
the-djmaze
2a45142644 Resolve #1185 2023-06-22 12:42:59 +02:00
the-djmaze
663f86ccda
Merge pull request #1189 from ner00/master
Squire UI - Editor strings
2023-06-22 12:22:00 +02:00
ner0
867c319a1e Squire UI - Editor strings 2023-06-21 18:41:28 +01:00
the-djmaze
5cce4713a3
Merge pull request #1183 from hguilbert/patch-10
Update admin.json
2023-06-21 08:09:54 +02:00
the-djmaze
1e29f03c03
Merge pull request #1184 from hguilbert/patch-11
Update user.json
2023-06-21 08:09:44 +02:00
hguilbert
de45fe62cc
Update user.json
Translation update
2023-06-19 17:29:21 +02:00
hguilbert
12102fdfdd
Update admin.json
Translation update
2023-06-19 17:11:34 +02:00
the-djmaze
954a86b23d Resolve #1180 2023-06-19 16:38:00 +02:00
the-djmaze
0f5e16f7d8 Update css/js file sizes 2023-06-19 16:20:20 +02:00
the-djmaze
21f3661c8d v2.28.2 2023-06-19 15:32:25 +02:00
the-djmaze
821b175391 https://github.com/the-djmaze/snappymail/discussions/1043#discussioncomment-6166617 2023-06-19 14:36:45 +02:00
the-djmaze
a022f90cef Bugfix: base64_decode() second parameter must be true 2023-06-19 14:17:06 +02:00
the-djmaze
529223a43e Added JWT class 2023-06-19 14:16:42 +02:00
the-djmaze
b8ebe6c1bc Resolve #1179 2023-06-19 11:25:37 +02:00
the-djmaze
0e3025eb79
Merge pull request #1176 from LarsBel/master
add Nextcloud 27 compatibility
2023-06-18 23:47:01 +02:00
the-djmaze
0905cc4813
Merge pull request #1175 from tinola/master
update Polish translation
2023-06-18 23:46:49 +02:00
Rene Hampölz
0d20305a27 chore: Update input & select styles from upstream nextcloud 2023-06-18 19:59:33 +00:00
Rene Hampölz
71c99dad5a chore: Rework emailaddresses select 2023-06-18 19:56:03 +00:00
Rene Hampölz
534a87b254 chore: Add tablet layout (wip) 2023-06-17 19:38:19 +00:00
Rene Hampölz
72ba59e95b refactor: Cleanup 2023-06-17 18:56:24 +00:00
Rene Hampölz
92098cb60f chore: Improve highlight folder row keyframe 2023-06-17 18:51:33 +00:00
Rene Hampölz
d197636eda fix: Mobile dropdown positioning 2023-06-17 18:29:57 +00:00
Rene Hampölz
5cd879484a fix: Emailaddresses input width on mobile 2023-06-17 16:19:52 +00:00
Rene Hampölz
26547f48b6 fix: Contact popup border 2023-06-16 20:16:02 +00:00
Rene Hampölz
7fa19917ff fix: Contact popup coloring 2023-06-16 20:03:52 +00:00
Rene Hampölz
c9c0d0d888 chore: Improve mobile layout of contact popup 2023-06-16 20:01:36 +00:00
Rene Hampölz
e515163a90 chore: Remove admin settings toolbar text-shadow 2023-06-15 16:53:33 +00:00
Rene Hampölz
6310e9a39d chore: Add compose popup attachments-count badge 2023-06-15 16:49:25 +00:00
Rene Hampölz
3fedf8a053 chore: Improve mobile placement of checkboxes in AdvancedSearch popup 2023-06-15 16:39:44 +00:00
Rene Hampölz
1ccef6cb26 fix: Mobile popup padding 2023-06-15 15:48:51 +00:00
Rene Hampölz
d0b00887b4 chore: Improve admin settings mobile layout 2023-06-15 11:38:24 +00:00
Rene Hampölz
d3609d04e4 fix: Broken Nextcloud menu list margin 2023-06-15 09:26:56 +00:00
Rene Hampölz
2fdc9a2308 fix: Mobile side panel border color 2023-06-14 20:49:38 +00:00
Rene Hampölz
8470525f99 fix: OpenPgpImport popup textarea width 2023-06-14 20:47:46 +00:00
Rene Hampölz
5800905f01 fix: Languages popup label margin 2023-06-14 20:47:46 +00:00
Rene Hampölz
75fb5170c1 fix: Mobile dropdown positioning 2023-06-14 20:47:45 +00:00
Rene Hampölz
819147246f fix: Tabs label overflow 2023-06-14 20:11:01 +00:00
LarsBel
02210fb827
Merge pull request #1 from LarsBel/NC27compatibility
add Nextcloud 27 compatibility
2023-06-14 21:35:17 +02:00
LarsBel
41d118b99b
add Nextcloud 27 compatibility 2023-06-14 21:33:51 +02:00
tinola
bd1192d716 fix typo 2023-06-14 09:10:05 +02:00
tinola
56837256fa update Polish translation 2023-06-14 09:08:00 +02:00
the-djmaze
9e2b5202c9
Merge pull request #1174 from cm-schl/master
German translation of editor buttons
2023-06-13 22:50:43 +02:00
cm-schl
be4fc15cb0
German translation of editor buttons 2023-06-13 16:12:11 +02:00
the-djmaze
ed31ed206d Translate Squire UI 2023-06-13 14:22:20 +02:00
the-djmaze
5bf0f3b562 Improvements for #1071 2023-06-13 13:44:38 +02:00
the-djmaze
3a3b2f0771 Prevent MessageList multiple request at the same time #1071 2023-06-12 17:00:23 +02:00
the-djmaze
fabc162481 Bugfix: invalid CSP report-uri 2023-06-12 14:13:42 +02:00
the-djmaze
9ff3e94870 Resolve #1169 2023-06-12 13:12:13 +02:00
the-djmaze
311dc7610f Merge handling of local Account Settings
Found while investigating #1170
2023-06-12 12:15:11 +02:00
the-djmaze
1dd0f546ce As BIDI is added, remove from README.md #1158 2023-06-12 11:49:12 +02:00
the-djmaze
3b682f5811 Added BIDI to Squire #1158 2023-06-12 11:45:59 +02:00
the-djmaze
43d885b922 Nextcloud Cannot modify header information
https://github.com/the-djmaze/snappymail/issues/929#issuecomment-1582552784
2023-06-12 10:58:56 +02:00
the-djmaze
fc6a58fd5d
Merge pull request #1161 from cm-schl/master
Update German admin.json
2023-06-08 14:32:55 +02:00
the-djmaze
f7ff9d662d Merge branch 'master' of github.com:the-djmaze/snappymail 2023-06-08 14:32:12 +02:00
the-djmaze
1d6be7ae52 Detailed error for "Cannot access the repository at the moment" #1164 2023-06-08 14:31:57 +02:00
Rene Hampölz
0050a8f4ad fix: Dropdown coloring of select element 2023-06-06 20:24:05 +00:00
Rene Hampölz
b8b813e605 chore: Use same styles for input and select elements 2023-06-06 20:10:58 +00:00
Rene Hampölz
c79f9ab2a0 fix: Weird icon buttons 2023-06-06 19:58:05 +00:00
the-djmaze
336fdf074a
Merge pull request #1160 from hguilbert/patch-9
Update user.json
2023-06-06 18:52:07 +02:00
cm-schl
5947ae9308
Update German admin.json 2023-06-06 14:48:51 +02:00
hguilbert
f49874c10e
Update user.json
Some update
2023-06-06 08:00:17 +02:00
Rene Hampölz
2327e812a8 fix: Input border color 2023-06-05 22:11:52 +00:00
Rene Hampölz
c00a1e41d6 chore: Improve message list toolbar on mobile 2023-06-05 21:50:17 +00:00
Rene Hampölz
88a6ab3e11 fix: Inconsistent buttons 2023-06-05 21:28:06 +00:00
Rene Hampölz
a25c43f340 chore: Change dropdown text align 2023-06-05 18:38:36 +00:00
Rene Hampölz
ebe952e005 chore: Adjust fallback border radius of grouped button 2023-06-05 18:36:24 +00:00
Rene Hampölz
8416e4e1cd refactor: Full restructure of styles file 2023-06-05 18:34:47 +00:00
Rene Hampölz
09f57db753 fix: Button group fallback styles 2023-06-05 15:20:03 +00:00
Rene Hampölz
6a6d836f9e fix: Button group fallback styles 2023-06-05 14:15:39 +00:00
the-djmaze
fe0216aa3a v2.28.1 2023-06-05 15:49:16 +02:00
the-djmaze
d190aa114b Resolve #1149 2023-06-05 15:31:01 +02:00
the-djmaze
15eaf18ec3 Resolve #1151 2023-06-05 15:12:22 +02:00
the-djmaze
14edb6c18e Merge branch 'master' of github.com:the-djmaze/snappymail 2023-06-05 15:00:51 +02:00
the-djmaze
71a2b46d14 clearstatcache() for #1155 2023-06-05 15:00:42 +02:00
the-djmaze
37f014ff74
Merge pull request #1157 from mayswind/master
update Chinese translation
2023-06-05 14:22:58 +02:00
the-djmaze
ae697bc06a
Merge pull request #1156 from tinola/master
Add polish translation
2023-06-05 14:22:49 +02:00
Rene Hampölz
135cf1d3df fix: RTL selector maybe causes issues with non-rtl styles in some browsers 2023-06-04 08:42:27 +00:00
tinola
1d47f36573 fix last commit 2023-06-03 19:06:48 +02:00
tinola
d582e4508f Name in Polish was incorrect 2023-06-03 19:03:50 +02:00
tinola
e81ebc4460 another fixes, should be ok for now :) 2023-06-03 18:36:11 +02:00
tinola
924c6f5e7d yet another small fix ;) 2023-06-02 20:57:45 +02:00
tinola
36ccb5a962 just another fix in Polish translation :) 2023-06-02 20:32:00 +02:00
tinola
cb35387fa3 correction in Polish translation 2023-06-02 20:25:22 +02:00
tinola
b9ae32a901 update Polish translation 2023-06-02 20:04:26 +02:00
MaysWind
aae8a4f771 update Chinese translation 2023-06-03 01:26:12 +08:00
tinola
c2f8eec411 Add polish translation 2023-06-02 18:43:59 +02:00
Rene Hampölz
ffb8fe4c1b fix: Light theme backdrop message color 2023-06-01 16:42:18 +00:00
Rene Hampölz
933548dd79 chore: Improve folders text-overflow on disabled panel 2023-06-01 07:31:56 +00:00
Rene Hampölz
d1039cbcb7 fix: Too small messageList min-width 2023-06-01 07:15:36 +00:00
Rene Hampölz
db2b630ed7 chore: Improve threads badge margin 2023-05-31 22:01:22 +00:00
Rene Hampölz
9831b23a5e chore: Improve search-input-wrp 2023-05-31 21:50:24 +00:00
the-djmaze
9eaa0f4eb5 Resolve #1147 2023-05-31 21:44:51 +02:00
Rene Hampölz
dbe01cb15e fix: Broken styles in RTL layout 2023-05-31 15:25:54 +00:00
Rene Hampölz
704dd518cb chore: Move theme to global scope 2023-05-31 14:03:13 +00:00
Rene Hampölz
2776e4c10a chore: Update preview image 2023-05-31 14:02:13 +00:00
Rene Hampölz
454cc5a0c8 chore: Improve theme selector 2023-05-31 12:54:53 +00:00
Rene Hampölz
cb42124eb2 chore: Use Nextcloud variables otherwise fallback to default 2023-05-31 12:41:00 +00:00
Rene Hampölz
a65415a276 docs: Improve theme passthrough code comments 2023-05-31 10:35:24 +00:00
Rene Hampölz
3d3e6d0961 fix: Layout issues in horizontal view 2023-05-31 10:04:45 +00:00
Rene Hampölz
ebd474eaef fix: Contact popup styles in latest snappymail release 2023-05-31 09:51:13 +00:00
Rene Hampölz
55bf922edb fix: New unread sub-messages badge for threaded view 2023-05-31 09:44:10 +00:00
Rene Hampölz
6878d4c0de fix: Broken checkbox 2023-05-30 18:45:27 +00:00
the-djmaze
3c28bc6b97 v2.28.0 2023-05-30 17:02:47 +02:00
the-djmaze
7404ee058f Orange border for unread thread sub message(s) 2023-05-30 16:13:59 +02:00
the-djmaze
26fb2cf9fb Resolve #1028 2023-05-30 16:02:41 +02:00
the-djmaze
1f8fddd3f4 Bugfix: show timeout/abort error for #1071 2023-05-30 14:48:12 +02:00
the-djmaze
b22d16a2b6 Resolve #1137 2023-05-30 14:24:45 +02:00
Rene Hampölz
a6638e56fe chore: Add messages drag tip 2023-05-30 11:01:00 +00:00
Rene Hampölz
a602dfdf04 chore: Add attachment-place dragover 2023-05-30 10:47:30 +00:00
Rene Hampölz
0f11e81c23 refactor: Remove dead code 2023-05-30 10:39:01 +00:00
Rene Hampölz
9e7dc8b1e9 chore: Add message-list dragover 2023-05-30 10:36:38 +00:00
Rene Hampölz
58c90f4888 fix: Wrong message-list error color 2023-05-30 10:32:37 +00:00
Rene Hampölz
d7d8fc2314 refactor: Improve structure 2023-05-30 10:27:18 +00:00
Rene Hampölz
c191489891 fix: Wrong message-view error color 2023-05-30 10:04:18 +00:00
Rene Hampölz
183f033c7e chore: Improve email address select 2023-05-30 10:03:44 +00:00
the-djmaze
bdbb1f8bbf Resolve #1141 2023-05-30 12:01:30 +02:00
Rene Hampölz
12d1d1acae fix: Styles in next snappymail release 2023-05-30 09:43:08 +00:00
Rene Hampölz
96d4ae51e3 fix: Non-standard width properties 2023-05-29 19:44:08 +00:00
Rene Hampölz
cbbcaec957 fix: Popup footer button margin 2023-05-29 19:17:27 +00:00
Rene Hampölz
10c10bb104 chore: Improve compose popup button margin 2023-05-29 19:16:55 +00:00
Rene Hampölz
11770b59df chore: Improve error tip 2023-05-29 08:18:41 +00:00
Rene Hampölz
9ab9a6c9e5 chore: Remove "no attachments" text shadow 2023-05-28 16:44:16 +00:00
Rene Hampölz
23572a1272 chore: Improve attachment items 2023-05-28 16:41:37 +00:00
the-djmaze
0899dea56f Fix composer style for https://github.com/the-djmaze/snappymail/issues/1138#issuecomment-1565313259 2023-05-27 22:26:47 +02:00
the-djmaze
828c1eb5a2 Solve fatal error for https://github.com/the-djmaze/snappymail/issues/1138#issuecomment-1565336950 2023-05-27 22:14:42 +02:00
Rene Hampölz
62cf66b8ec chore: Improve nextcloud file picker 2023-05-27 15:08:50 +00:00
Rene Hampölz
52227a8e29 fix: Unread badge for user-folders 2023-05-27 08:37:08 +00:00
Rene Hampölz
82f7c3f4b4 fix: Login input icons 2023-05-26 19:00:18 +00:00
Rene Hampölz
c10e84f68a chore: Don't collapse settings side panel 2023-05-26 18:51:46 +00:00
Rene Hampölz
5fb914fbc7 chore: Improve compose popup header 2023-05-26 18:32:00 +00:00
Rene Hampölz
69b96edbde chore: Add nextcloud calendar select 2023-05-26 09:10:58 +00:00
Rene Hampölz
8f4fd6f214 fix: Button focus text color 2023-05-26 08:53:29 +00:00
Rene Hampölz
10d2ef334f fix: Link coloring 2023-05-26 08:43:02 +00:00
Rene Hampölz
e7bc290e15 chore: Hide additional theme settings selectively 2023-05-26 08:35:36 +00:00
Rene Hampölz
f17bb54e6f chore: Add contacts 2023-05-25 17:41:41 +00:00
Rene Hampölz
fd2f7e8c9d fix: Striped table 2023-05-24 10:36:52 +00:00
Rene Hampölz
78cc010560 chore: Improve link coloring 2023-05-24 10:35:48 +00:00
Rene Hampölz
f124207072 fix: Danger button 2023-05-23 16:06:15 +00:00
the-djmaze
e90ecd472c Merge branch 'master' of github.com:the-djmaze/snappymail 2023-05-23 13:54:14 +02:00
the-djmaze
197f597d1c Resolve #1134 2023-05-23 13:53:44 +02:00
Rene Hampölz
0a2c19a186 fix: MailMessageView button toolbar padding 2023-05-23 11:05:16 +00:00
Rene Hampölz
3c57c2e593 fix: fix: MessageView edit button selector 2023-05-23 10:24:41 +00:00
Rene Hampölz
93c6a4f122 fix: MessageView edit button 2023-05-23 10:23:22 +00:00
Rene Hampölz
4fe9cc5523 chore: Improve buttons 2023-05-23 09:49:52 +00:00
Rene Hampölz
3713d0f054 fix: Consider hidden toolbar buttons 2023-05-22 21:47:21 +00:00
Rene Hampölz
243baf5b5e chore: Rework button toolbar 2023-05-22 17:13:11 +00:00
Rene Hampölz
40bcf6b94b chore: Improve tabs 2023-05-22 13:12:01 +00:00
Rene Hampölz
e5c0e50c8c fix: Dropdown menu scrollbar 2023-05-22 10:27:22 +00:00
Rene Hampölz
a6da438ae7 fix: Account place border color 2023-05-22 10:17:21 +00:00
Rene Hampölz
55b872ee03 feat: Add language popup 2023-05-22 10:14:27 +00:00
Rene Hampölz
cbbf673f4d chore: Add plugin popups 2023-05-22 09:44:56 +00:00
Rene Hampölz
804b9531f7 chore: Improve settings control-group margin 2023-05-22 09:44:24 +00:00
Rene Hampölz
df04a6dc28 fix: Popup button margin 2023-05-22 08:53:53 +00:00
Rene Hampölz
c50642021b fix: Checkbox margin 2023-05-22 08:46:22 +00:00
Rene Hampölz
e179b46140 chore: Add tabs 2023-05-21 19:19:50 +00:00
Rene Hampölz
9cdbb609eb fix: Minor popup styles 2023-05-21 19:19:02 +00:00
the-djmaze
3340e0ba9f Left side menu position for RTL #1056 2023-05-18 16:52:24 +02:00
the-djmaze
24f7984d82
Merge pull request #1135 from jaapmarcus/patch-1
Fix password reset Hestia
2023-05-18 13:33:55 +02:00
Jaap Marcus
4cee246281
Update index.php 2023-05-17 22:33:06 +02:00
Jaap Marcus
0ae7b11776
Fix HestiaCP password change 2023-05-17 22:28:03 +02:00
the-djmaze
02576302c9 CSS unicode-bidi #1056 2023-05-16 17:28:15 +02:00
the-djmaze
6dec3c9052 Hestia plugin v1.1 for #1128 2023-05-16 16:57:21 +02:00
the-djmaze
08f27c1ec2 fix hestia script path 2023-05-16 16:53:17 +02:00
the-djmaze
d5a0ac8da8 Fixes for #1128 2023-05-16 16:51:42 +02:00
the-djmaze
35bfff878d Changes for #1128 2023-05-16 13:39:56 +02:00
the-djmaze
a504fd9298 Resolve #1130 2023-05-16 12:20:45 +02:00
the-djmaze
0bd5603dd0 Merge branch 'master' of github.com:the-djmaze/snappymail 2023-05-15 15:16:29 +02:00
the-djmaze
a2ca00e3e8 Log more for #1080 2023-05-15 15:16:10 +02:00
the-djmaze
59aa1cac35 Resolve #1125 2023-05-12 23:43:13 +02:00
the-djmaze
2c014caa72 Resolve #1126 2023-05-12 23:21:55 +02:00
the-djmaze
ffde5ddfb3 Bugfix: addEventListener('rl-view-model') missing for Settings #1013 2023-05-09 12:31:29 +02:00
the-djmaze
7968518f9c Compose window RTL #1056 2023-05-09 12:11:38 +02:00
the-djmaze
13abe74a31 Resolve #1119 2023-05-09 12:07:07 +02:00
the-djmaze
216b5964d7
Merge pull request #1124 from ner00/master
Update pt/pt-PT translation
2023-05-09 11:44:02 +02:00
the-djmaze
759ed2ad36 Fix tabs labels row height 2023-05-09 11:27:40 +02:00
ner0
51d5c1e19f Update pt/pt-PT translation 2023-05-08 18:28:05 +01:00
the-djmaze
08e8a570b8 Fix Nextcloud .btn-group > .btn layout 2023-05-08 14:41:45 +02:00
the-djmaze
e0c21b4b85 Resolve #1117 2023-05-08 14:41:06 +02:00
the-djmaze
bf8182f19b Added CSS --btn-border-radius 2023-05-08 14:37:13 +02:00
the-djmaze
8b4674b87f MultipleStreamWriter() to single WriteStream() 2023-05-08 14:20:04 +02:00
the-djmaze
2d52493b89 Fix locale pagination order for #1056 2023-05-08 13:23:54 +02:00
the-djmaze
bf6f342ffd Make pagination numbers locale aware for #1056 2023-05-08 13:01:16 +02:00
the-djmaze
85bb5348c0 Resolve #1104 2023-05-08 12:21:17 +02:00
the-djmaze
674e842861 Change for #1013 2023-05-08 10:59:11 +02:00
the-djmaze
eab3beaf2a Bugfix: node is null 2023-05-08 10:36:23 +02:00
the-djmaze
7b70117155 Resolve #1123 2023-05-08 09:47:08 +02:00
the-djmaze
a043eaec81 Change for #1105 2023-05-01 15:21:04 +02:00
the-djmaze
32277688af Resolve #1109 2023-05-01 12:01:35 +02:00
the-djmaze
88279c55e0 Resolve #1111 2023-05-01 11:48:57 +02:00
the-djmaze
932b71f345 Resolve #1112 2023-05-01 11:13:51 +02:00
the-djmaze
9021d52096
Merge pull request #1107 from chiyi4488/patch-1
Update zh-TW translation
2023-04-30 11:11:23 +02:00
the-djmaze
dc03d04658
Merge pull request #1108 from konkere/ru_localization
Update ru-RU translation.
2023-04-30 11:11:14 +02:00
chiyi4488
2dbd851fe7
Update user.json 2023-04-30 14:47:57 +08:00
Konkere
17f060316d Update ru-RU translation.
The variable "GLOBAL" - "NAME" is also used in the folder creation menu.
2023-04-27 11:58:58 +03:00
chiyi4488
17dbbecf5e
Update user.json 2023-04-27 12:15:39 +08:00
chiyi4488
6cca678ece
Update user.json 2023-04-27 08:45:50 +08:00
chiyi4488
66e4a1bb61
Update user.json 2023-04-27 08:45:16 +08:00
chiyi4488
b4a42a1681
Update zh-TW l10n 2023-04-27 01:03:49 +08:00
the-djmaze
404778089b I broke code with previous commit 2023-04-24 12:03:03 +02:00
the-djmaze
1883b407a6 TODO: Sieve issue on account switch
When current domain has sieve but the new has not, or current has not and the new has
2023-04-24 12:01:09 +02:00
the-djmaze
80d64574a6 Changes for #1078 2023-04-24 11:19:07 +02:00
the-djmaze
6e0372dcee Additions for #1094 2023-04-23 16:45:19 +02:00
the-djmaze
90365940ff
Merge pull request #1102 from hguilbert/patch-8
Update user.json
2023-04-23 16:40:35 +02:00
hguilbert
84d139bb25
Update user.json
Some update FR
2023-04-22 09:30:21 +02:00
the-djmaze
287b9b262b
Merge pull request #1100 from konkere/ru_localization
Fix and add translation's items in ru localization
2023-04-21 21:26:47 +02:00
Konkere
60c6777e2d Fix and add translation's items in ru localization 2023-04-21 15:01:58 +03:00
the-djmaze
ad6b51f76f Resolve #1083 2023-04-18 13:20:52 +02:00
the-djmaze
5eee165377
Merge pull request #1094 from cm-schl/master
added some italian translation - needs review
2023-04-18 13:06:42 +02:00
the-djmaze
178d65d12c Shorten arab translation for "Spam" 2023-04-18 11:48:31 +02:00
the-djmaze
a9a49a2d6f Also login RTL #1056 2023-04-18 11:43:28 +02:00
the-djmaze
a71ea44753 Improved RTL support #1056 2023-04-18 11:35:31 +02:00
cm-schl
4e6b4b2e0a Corrected translations - thanks to @lota 2023-04-18 10:44:42 +02:00
the-djmaze
9cabfe7570 Merge branch 'master' into rtl-support 2023-04-18 09:54:24 +02:00
the-djmaze
ad07c5b472 Changes for #1046 2023-04-18 09:45:03 +02:00
the-djmaze
c7cc250b1e Resolve #1086 2023-04-18 09:31:14 +02:00
the-djmaze
8630b418d1 Resolve #1082 2023-04-18 09:22:04 +02:00
cm-schl
a893455097 added some italian translation - needs review 2023-04-17 17:18:36 +02:00
the-djmaze
b09be12a37
Merge pull request #1087 from cm-schl/master
Update German translation
2023-04-17 11:47:56 +02:00
cm-schl
e69bcd1970 Update German translation 2023-04-14 12:45:33 +02:00
the-djmaze
628770b6c1
Resolve #1085 2023-04-13 23:06:29 +02:00
the-djmaze
2adccdbaf3 Allow empty message body when there are attachments #1052 2023-04-11 14:16:47 +02:00
the-djmaze
103b431780 Bugfix: GetActionParam() failed 2023-04-11 13:57:28 +02:00
the-djmaze
a889fc262a Nextcloud plugin integrate OIDC from #756 2023-04-11 13:28:29 +02:00
the-djmaze
14ee6f1b5b When reporting a bug, as user for usage mode 2023-04-11 13:02:26 +02:00
the-djmaze
20a2f5f185 Added base code for RTL support #1056 2023-04-11 12:55:59 +02:00
the-djmaze
dfc415e3a8 Allow when listLimited is active 2023-04-11 11:24:15 +02:00
the-djmaze
0a3effc6ba Inherit logger as Trait 2023-04-11 10:56:28 +02:00
the-djmaze
b5b1d5f88a Resolve #1070 2023-04-11 09:50:37 +02:00
the-djmaze
c7c43939ce Update ChangePassword version number 2023-04-11 09:18:00 +02:00
the-djmaze
9689b480a0 Move ChangePassword ISPConfig to own extension 2023-04-11 09:14:59 +02:00
the-djmaze
e8a5d09b97 Resolve #1069 2023-04-04 14:30:35 +02:00
the-djmaze
f686e9e875 v2.27.3 2023-04-04 12:28:35 +02:00
the-djmaze
b0dda8d3ae Allow saving draft with empty body #1052 2023-04-04 12:22:51 +02:00
the-djmaze
af9b778f63 Log \nonexistent folder for #1008 2023-04-04 12:18:58 +02:00
the-djmaze
87313d0616 Resolve #1003 2023-04-04 11:57:52 +02:00
the-djmaze
944a2cb8dc A solution for #1056 to support <html data-date-lang="fa-IR" 2023-04-04 11:28:15 +02:00
the-djmaze
fadba0f36a Resolve #1022 2023-04-04 11:09:57 +02:00
the-djmaze
ad56ca0f08 Resolve #1065 2023-04-04 10:42:18 +02:00
the-djmaze
ea8a9a0741 Resolve #1060 2023-04-04 10:36:28 +02:00
the-djmaze
2e0fc1585d Improved Squire by visualizing some active buttons states 2023-04-04 10:15:18 +02:00
the-djmaze
baacddd324 Bugfix: GnuPG decrypt failed 2023-04-03 10:29:42 +02:00
the-djmaze
da2ab68fcc Cleanup some enums to consts 2023-04-03 10:29:26 +02:00
the-djmaze
91e60c1007 Resolve #1037 2023-04-03 09:35:56 +02:00
the-djmaze
f3845be39f Fix GD2 for #1009 2023-03-28 15:06:05 +02:00
the-djmaze
dd74759adc Workaround #1046 2023-03-28 13:54:22 +02:00
the-djmaze
28cd8245c4 Resolve #1048 2023-03-28 13:42:35 +02:00
the-djmaze
564afd96f2 Resolve #1004 and #1054 due to #962 2023-03-28 13:13:44 +02:00
the-djmaze
78e294e98e Check if $rImapLiteralStream is open resource and not closed 2023-03-28 10:02:49 +02:00
the-djmaze
9a4da9d7f0 Use Actions()->decodeRawKey() 2023-03-28 10:02:22 +02:00
the-djmaze
3819311a6d Merge branch 'master' of github.com:the-djmaze/snappymail 2023-03-28 09:50:17 +02:00
the-djmaze
0c89285033 Resolve #1057 2023-03-28 09:49:25 +02:00
the-djmaze
7106ffceb3
Merge pull request #1045 from hguilbert/patch-7
Update user.json
2023-03-27 12:57:34 +02:00
hguilbert
c589e24aeb
Update user.json 2023-03-22 18:15:25 +01:00
the-djmaze
f33ae5e021 v2.27.2 2023-03-22 07:34:58 +01:00
the-djmaze
b5356a4115 Improve #1038 2023-03-21 19:08:17 +01:00
the-djmaze
30c95ac04e SASL always base64 by default 2023-03-21 18:56:48 +01:00
the-djmaze
7105e23781 Resolve #1038 2023-03-21 18:55:38 +01:00
the-djmaze
a702a45086 Resolve #1024 2023-03-21 18:29:18 +01:00
the-djmaze
68cf58e9da
Merge pull request #1036 from cm-schl/master
Rewrite of plugin ldap-mail-accounts
2023-03-21 15:53:30 +01:00
the-djmaze
cf2c3b944e Change for #1024 2023-03-21 15:49:33 +01:00
cm-schl
bf54ed90a7
Merge branch 'the-djmaze:master' into master 2023-03-21 15:27:49 +01:00
the-djmaze
8e8f396c46 v2.27.1 2023-03-21 14:00:55 +01:00
the-djmaze
f1f4606cc6 Resolve #1032 2023-03-21 12:21:37 +01:00
the-djmaze
db5bd3cd36 Improved attachmentIcon glyph coloring 2023-03-21 11:05:34 +01:00
the-djmaze
b29d8f617c Better align .accountPlace text 2023-03-21 10:06:00 +01:00
the-djmaze
50487af6a7 Improved attachment glyph handling 2023-03-21 10:05:29 +01:00
cm-schl
c2b7ddc76b
Merge branch 'the-djmaze:master' into master 2023-03-21 09:20:15 +01:00
the-djmaze
6b29460bdf Change for #1025 2023-03-20 22:38:02 +01:00
the-djmaze
e14719b021 Resolve #1027 2023-03-20 22:22:37 +01:00
the-djmaze
1ca039f796 v2.27.0 2023-03-20 16:43:41 +01:00
the-djmaze
45be0d7aa8 Bugfix: ask unload when mobile and message visible 2023-03-20 15:21:56 +01:00
the-djmaze
470da2ee15 Nextcloud CSS --panel-bg-clr was missing 2023-03-20 08:32:57 +01:00
the-djmaze
f81fcb4e61 Better line-height for QR code 2023-03-20 08:32:31 +01:00
the-djmaze
60dca97603 Bugfix: when decrypt message, subject was replaced empty 2023-03-20 08:27:53 +01:00
the-djmaze
7a98e5e7d4 Nextcloud plugin translate russian
https://github.com/the-djmaze/snappymail/discussions/979#discussioncomment-5327668
2023-03-18 19:32:50 +01:00
the-djmaze
e30f9854b7 Fix theme cached handling 2023-03-17 23:20:42 +01:00
the-djmaze
b85ed63ff6 Resolve #1018 2023-03-15 23:04:58 +01:00
the-djmaze
dc06502744 Resolve #1017 2023-03-15 22:53:06 +01:00
cm-schl
75f198c8a0 Update release date 2023-03-14 14:50:22 +01:00
cm-schl
00a507ff97
Merge branch 'the-djmaze:master' into master 2023-03-14 14:48:42 +01:00
cm-schl
7662889237
Merge pull request #1 from cm-schl/rework-on-mailaddress-handling
Rework on mailaddress handling
2023-03-14 14:48:17 +01:00
cm-schl
2d4e72805a Updated readme.md 2023-03-14 14:37:37 +01:00
the-djmaze
308c7197c7 Resolve #1007 2023-03-14 13:55:20 +01:00
the-djmaze
014d22add9 PHP getElementsByTagName() must be lowercase #1015 2023-03-14 13:05:46 +01:00
the-djmaze
8ea2ff4ae8 Resolve #968 2023-03-14 12:46:12 +01:00
the-djmaze
84c4522ff2
Merge pull request #1011 from mayswind/master
update Chinese translation
2023-03-14 10:55:18 +01:00
the-djmaze
4bd9535219 ERROR: Undefined constant "MailSo\Log\Drivers\STDERR" #965 2023-03-14 10:53:18 +01:00
the-djmaze
2201060888 Changes for #1007 2023-03-14 10:46:52 +01:00
the-djmaze
b824c394ab Cache the index 2023-03-14 09:58:58 +01:00
the-djmaze
3464e296c3 Fix 'Location:' headers using proper '302 Found' header 2023-03-14 09:58:30 +01:00
the-djmaze
6fde436865 Don't generateETag() on SELECT/EXAMINE 2023-03-14 09:39:33 +01:00
the-djmaze
a0a7e93707 Resolve #1014 2023-03-13 21:48:20 +01:00
the-djmaze
85b06123c6 Better hash/cache handling for some functions 2023-03-13 15:21:37 +01:00
the-djmaze
321782aa26 Potential fix for #1004 2023-03-13 15:21:06 +01:00
the-djmaze
49f76ffe76 Move more JSON code into MailSo\Mail\Message 2023-03-13 13:01:05 +01:00
the-djmaze
a019f4d80e Reset folder etag on draft send 2023-03-13 12:59:16 +01:00
the-djmaze
1923d5cb4b Speedup CheckableFolders 2023-03-13 12:04:09 +01:00
the-djmaze
f8b794ec1c Prevent folder/messages flags conflict by using the right name attributes for Folders 2023-03-13 11:49:40 +01:00
the-djmaze
17b1deb95b FolderInformation() use jsonSerialize() 2023-03-13 10:46:40 +01:00
the-djmaze
59b3fe70b6 Improved mailbox ETag 2023-03-13 09:56:35 +01:00
the-djmaze
2dbf192558 Knockout added future code replaceChildren() for Safari 14+ 2023-03-13 09:03:14 +01:00
the-djmaze
4a4b2938a6 Remove unused GetConnectedPort() 2023-03-13 09:02:18 +01:00
MaysWind
51fe76cca3 update Chinese translation 2023-03-12 21:24:11 +08:00
the-djmaze
1b49399c46 Resolve #1006 2023-03-10 19:38:09 +01:00
the-djmaze
670271f257 Resolve #1005 2023-03-07 20:03:50 +01:00
the-djmaze
d484392518 Improved mailvelope handling 2023-03-07 16:06:09 +01:00
the-djmaze
4a9bc53558 Fix PGP/Inline view 2023-03-07 15:42:13 +01:00
cm-schl
c732cec62f Clearer description whats username for in the config 2023-03-07 15:31:09 +01:00
the-djmaze
f5045b9759 Move APP_VERSION hashing to smctoken encryption key 2023-03-07 15:14:49 +01:00
cm-schl
58b368552f Overwrite of main address works
- Cleanup of not used functions
- Rewrite of FindLdapResults function to work in both cases: for additional accounts and main account
2023-03-07 15:05:10 +01:00
the-djmaze
9dbf7b3bad Remove unused KeyPathHelper::TemplatesCache() and KeyPathHelper::PublicFile() 2023-03-07 14:41:42 +01:00
the-djmaze
bd7f9856f2 Redesign Raw handling as encodeRawKey/decodeRawKey doesn't need encryption 2023-03-07 13:53:52 +01:00
the-djmaze
402de0d569 Bugfix: ETag should use account not ImapClient as switching accounts could generate same hash 2023-03-07 11:41:47 +01:00
the-djmaze
e2ed87a1ab Remove Model\Account param from encodeRawKey/decodeRawKey 2023-03-07 08:03:15 +01:00
the-djmaze
eaf4bbe6d3 Resolve more cache issue with #1003 2023-03-06 16:00:50 +01:00
the-djmaze
206c8abdf5 More changes for #1003 2023-03-06 13:21:21 +01:00
the-djmaze
8c7fd77762 Resolve #1001 2023-03-06 11:55:32 +01:00
the-djmaze
5436253ffd Cache changes for #1003 2023-03-06 11:36:23 +01:00
the-djmaze
4dbcade830 Knockout simplify Object.setPrototypeOf 2023-03-06 11:05:44 +01:00
the-djmaze
b7963ebfa0 Use UNSEEN in folder ETag #1003 2023-03-06 10:50:00 +01:00
the-djmaze
24cf17f68b Force Imap\Sync properties to be \MailSo\Imap\ImapClient 2023-03-03 22:41:26 +01:00
the-djmaze
17b6181b5d Remove MailClient->MessageAppendStream() in favour of ImapClient->MessageAppendStream() 2023-03-03 22:40:57 +01:00
the-djmaze
234a335425 Resolve #932 2023-03-03 16:20:38 +01:00
the-djmaze
44b8370f63 Bugfix: src is null 2023-03-03 16:10:29 +01:00
the-djmaze
33c10365f0 Improved FolderCollection handling 2023-03-03 15:50:27 +01:00
cm-schl
a8f6dab672 Working prototype.
- Needs check if overwriting mail address is activated
- Needs rework on ldap query function (looks up to much data)
- Code cleanup is needed
2023-03-03 12:27:56 +01:00
the-djmaze
a61ef0fa1c Merge branch 'master' of github.com:the-djmaze/snappymail 2023-03-03 10:51:37 +01:00
the-djmaze
da2e56a8cb MODSEQ requires 64-bit int 2023-03-03 10:51:16 +01:00
the-djmaze
dae15e26d8 Resolve #998 2023-03-03 10:50:53 +01:00
the-djmaze
502c3b873c
Merge pull request #994 from Akrobs/master
Update user.json & admin.json
2023-02-25 19:10:38 +01:00
Akrobs
40db956766
Update admin.json 2023-02-25 17:24:05 +03:00
the-djmaze
1663cc6ce5 Put top menu "accountPlace" inside top-system-dropdown-id 2023-02-25 15:10:21 +01:00
Akrobs
0f95bd6dc6
Update user.json 2023-02-25 17:06:29 +03:00
the-djmaze
9ef96fccdd Don't make font bigger when screen > 1400px 2023-02-25 14:32:45 +01:00
the-djmaze
661e66d7fd Put attachment controls inside attachmentsPlace 2023-02-25 14:24:42 +01:00
the-djmaze
4b11b5477e Allow Nextcloud 32bit 2023-02-25 14:23:47 +01:00
the-djmaze
d8b05ee361 Resolve #992 2023-02-25 14:16:04 +01:00
Rene Hampölz
a9d077a299 fix: Disable mail invert filter 2023-02-25 12:54:07 +00:00
Rene Hampölz
a93db065d6 fix: ShowImages layout 2023-02-25 12:44:41 +00:00
Rene Hampölz
dbaa6eb3b2 fix: Broken styles in the latest snappymail release 2023-02-25 12:39:49 +00:00
the-djmaze
0144be49bf Show message toolbar on screens > 1400px #970 2023-02-25 10:55:24 +01:00
the-djmaze
a8c7146f96 Resolve #991 2023-02-25 00:07:11 +01:00
the-djmaze
84008edf45 v2.26.4 2023-02-24 19:47:39 +01:00
the-djmaze
bc74ca34d3 Simplify CSS code 2023-02-24 18:31:03 +01:00
the-djmaze
f85e7b5dec Resolve #990 2023-02-24 12:38:19 +01:00
the-djmaze
e64d49240f Update and cleanup npm 2023-02-24 12:27:50 +01:00
the-djmaze
91b4ff16bd Reduce/simplify CSS footprint 2023-02-24 11:45:26 +01:00
the-djmaze
b03d35bc7a Resolve #989 2023-02-23 18:51:54 +01:00
the-djmaze
a86c5ba639 bugfix: unicode mailto: addresses 2023-02-23 18:43:27 +01:00
the-djmaze
d92f25973b fix unicode email address matching #955 2023-02-23 17:38:41 +01:00
the-djmaze
5ef79e5148 Also match unicode email addresses #955 2023-02-23 17:24:23 +01:00
the-djmaze
e3d2250e19 Nextcloud allow in iframe #537 2023-02-23 16:33:38 +01:00
the-djmaze
0d715c0bb4 Translate change for #988 2023-02-23 15:54:33 +01:00
the-djmaze
9fa145dbc5
Merge pull request #988 from HeySora/system-font
Use the System/Browser font by default
2023-02-23 15:50:48 +01:00
the-djmaze
d528498d8d Remove unused imports 2023-02-23 15:44:28 +01:00
the-djmaze
7f62d9b7aa remove unused aCache 2023-02-23 15:43:43 +01:00
the-djmaze
8623bb5d60 Reduce use of isMobile() 2023-02-23 15:43:32 +01:00
the-djmaze
6af13255f2 No need to listen for resize as matchMedia() is used now 2023-02-23 14:35:13 +01:00
HeySora
d8f6ed1370
Use the System/Browser font by default 2023-02-23 14:08:38 +01:00
the-djmaze
57f497cf63 isMobile() not used in templates 2023-02-23 14:04:35 +01:00
the-djmaze
e1833ae032 Make layout fully responsive using matchMedia('(max-width: 799px)') 2023-02-23 13:54:32 +01:00
the-djmaze
1dbd9bda0c Don't exit; 2023-02-23 09:04:44 +01:00
the-djmaze
202fb08d09 Move brotli and gzip compress option to application.ini 2023-02-23 08:39:40 +01:00
Rene Hampölz
d021bd26fd fix: MessageView toolbar padding 2023-02-22 19:18:01 +00:00
the-djmaze
b5db67d23f Bugfix some mobile CSS 2023-02-22 17:57:10 +01:00
the-djmaze
97282075b0 Reduce some JavaScript 2023-02-22 17:43:54 +01:00
the-djmaze
f023a0799c Probable fix for #860 2023-02-22 15:06:59 +01:00
the-djmaze
b93a31f950 scrollMessage not needed on focusedState change 2023-02-22 14:57:29 +01:00
the-djmaze
a80bb83ce9 Simplify CSS folder .focused 2023-02-22 14:53:12 +01:00
the-djmaze
ade75b4cbe Bugfix solution #537 for cached template 2023-02-22 14:13:18 +01:00
the-djmaze
c83cf64b80 Resolve #537 2023-02-22 14:09:26 +01:00
the-djmaze
bb77d0ae02 Redesign \SnappyMail\HTTP\CSP for better control 2023-02-22 13:57:37 +01:00
the-djmaze
915a142315 Merge branch 'master' of github.com:the-djmaze/snappymail 2023-02-22 12:19:04 +01:00
the-djmaze
f662841281 Resolve #987 2023-02-22 12:18:49 +01:00
the-djmaze
525535b7b8
Merge pull request #984 from ner00/master
Update pt/pt-PT
2023-02-22 09:24:56 +01:00
the-djmaze
f1c2e5be02
Merge pull request #985 from hguilbert/patch-5
Update admin.json
2023-02-22 09:24:47 +01:00
the-djmaze
b80cae0776
Merge pull request #986 from hguilbert/patch-6
Update user.json
2023-02-22 09:24:37 +01:00
the-djmaze
77126d989a Squire don't detect ftp links as browsers drop support 2023-02-22 08:52:20 +01:00
the-djmaze
d10aec8f08 Resolve #955 and allow unicode 2023-02-22 08:51:33 +01:00
hguilbert
fe411e0a29
Update user.json
Update translate FR
2023-02-22 06:13:00 +01:00
hguilbert
ce2e104b3e
Update admin.json
Update translate FR
2023-02-22 06:06:01 +01:00
ner0
8ea0693d80 Update 2023-02-21 23:21:31 +00:00
the-djmaze
4d8f7d2af7 No need for rl.initData 2023-02-21 20:05:02 +01:00
the-djmaze
145c11d58e Set X-Frame-Options: DENY when frame_ancestors is empty 2023-02-21 19:15:21 +01:00
Rene Hampölz
b1fef91c55 fix: SettingsPane margin 2023-02-21 17:16:29 +00:00
Rene Hampölz
1b10e304d4 fix: MessageList min width 2023-02-21 17:03:44 +00:00
Rene Hampölz
eb8bdd60c8 fix: Button overflow on small sidebar 2023-02-21 16:54:46 +00:00
the-djmaze
66fafd3981 Add CSP frame-ancestors for #537 2023-02-21 17:23:26 +01:00
the-djmaze
2daa4f97e2 v2.26.3 2023-02-21 16:55:10 +01:00
the-djmaze
0172931bf9 removed unused data.set('SignatureInsertBefore') in IdentityPopupView 2023-02-21 16:28:20 +01:00
the-djmaze
0f82ad1109 Cleanup Identity edit window 2023-02-21 16:26:21 +01:00
the-djmaze
d495369688 Cleanup IdentityModel handling 2023-02-21 16:02:22 +01:00
the-djmaze
bfffa750ca Translate "Are you sure you want to exit?" 2023-02-21 15:45:21 +01:00
the-djmaze
5050d8edb3 Improved folder name editing 2023-02-21 15:33:55 +01:00
the-djmaze
1c1b043a50 Improved key event handling 2023-02-21 14:26:37 +01:00
the-djmaze
81d521c562 Simplify folder name editing 2023-02-21 14:26:16 +01:00
the-djmaze
938c209869 Cleanup a bit of javascript 2023-02-21 12:59:56 +01:00
the-djmaze
0b8ec5c664 Bugfix: language selector didn't show current language in green 2023-02-21 11:53:43 +01:00
the-djmaze
d1823af111 More JSON properties to JavaScript camelCase 2023-02-21 11:52:50 +01:00
the-djmaze
07f6b7545a Rename spam=>junk and draft=>drafts folder names to be compatible with IMAP and JMAP 2023-02-21 10:56:21 +01:00
the-djmaze
6c00a9cf3b Save Settings array param to \RainLoop\Settings 2023-02-21 10:40:16 +01:00
the-djmaze
ce2fb9d7f2 Many AppData properties to JavaScript camelCase 2023-02-21 10:22:59 +01:00
the-djmaze
c53d9adaba Make allowCtrlEnterOnCompose an account setting (but inactive as never changed) 2023-02-21 09:18:26 +01:00
the-djmaze
618333e257 Removed unused allow_html_editor_biti_buttons config option 2023-02-21 09:07:11 +01:00
the-djmaze
8b70fee072 Moved rl.fetch and rl.fetchJSON to boot.js so that AppData can be fetched as JSON 2023-02-21 09:04:47 +01:00
the-djmaze
91bc5931fd Set default ContactsSync in Nextcloud 2023-02-21 08:38:19 +01:00
the-djmaze
15e25a16e4 Improved convert RainLoop ContactsSync data 2023-02-21 08:36:00 +01:00
the-djmaze
5a1835c576 Don't put ContactsSync PasswordHMAC in AppData 2023-02-21 08:35:29 +01:00
the-djmaze
cf720e7105 Resolve #981 2023-02-20 23:51:08 +01:00
the-djmaze
79edca93ef Resolve #982 2023-02-20 17:34:23 +01:00
the-djmaze
8d52b0afdf Bugfix: Notification enum conflicts with window.Notification 2023-02-20 15:59:37 +01:00
the-djmaze
b5ab175953 Merge GnuPG and OpenPGP.js passphrases 2023-02-20 15:49:38 +01:00
the-djmaze
8a8d1d756e centralize elementById('rl-app') 2023-02-20 15:47:46 +01:00
the-djmaze
ec3e99c6ab Newline at end of file 2023-02-20 15:36:01 +01:00
the-djmaze
c7e52e04c2 Small translate fix for previous commits 2023-02-20 15:35:38 +01:00
the-djmaze
08f391fcd7 bugfix: threads indicator gets lost with new message cache handling 2023-02-20 15:15:52 +01:00
the-djmaze
b8b7f10055 https://github.com/the-djmaze/snappymail/pull/965#discussion_r1111953373 2023-02-20 14:21:49 +01:00
Rene Hampölz
5dd0eaefe0 chore: Add textareas 2023-02-20 12:46:06 +00:00
the-djmaze
8a63ae05ef Cleanup unused Squire code 2023-02-20 13:12:09 +01:00
Rene Hampölz
9e29238ed1 fix: Hide number input's spin box on chrome 2023-02-20 12:00:26 +00:00
Rene Hampölz
f601e02bf7 chore: Add error tooltip 2023-02-20 11:53:34 +00:00
Rene Hampölz
d38bb34aae chore: Improve squire toolbar buttons 2023-02-20 11:52:24 +00:00
the-djmaze
e56d1cf5c6 Resolve #954 2023-02-20 11:50:11 +01:00
the-djmaze
3e99f760a4 Resolve #953 2023-02-20 11:25:20 +01:00
the-djmaze
4fb359be59 Improved messages cache handling and fixed broken messagesBodiesDom 2023-02-20 10:49:22 +01:00
Rene Hampölz
2ee8fa6480 chore: Remove hover effect for showImages area 2023-02-20 09:01:01 +00:00
Rene Hampölz
e388e6cd76 chore: Add nextcloud filepicker 2023-02-19 18:37:36 +00:00
Rene Hampölz
f19cfa850f fix: Some checkboxes are not styled 2023-02-19 17:28:48 +00:00
Rene Hampölz
19e24871e6 fix: Some buttons are not styled 2023-02-19 15:45:07 +00:00
Rene Hampölz
a4158393ea chore: Add loading and login screen 2023-02-19 15:33:57 +00:00
Rene Hampölz
6564e825ec Fix: Wrong colored scrollbar corner 2023-02-19 13:54:56 +00:00
Rene Hampölz
f8a2b3489a fix: Broken styles in the latest snappymail release 2023-02-19 13:48:17 +00:00
the-djmaze
15f9aa11db v2.26.2 2023-02-17 14:27:16 +01:00
the-djmaze
75a6353445 Remove CSS white-space from messages to prevent annoying side scrolling 2023-02-17 13:35:59 +01:00
the-djmaze
ab77949e64 Changes for #973 2023-02-17 13:35:21 +01:00
cm-schl
7948e86a07 added comment 2023-02-17 12:33:32 +01:00
cm-schl
b8c681d1a5 Additional accounts now always get the mail address from ldap 2023-02-17 12:01:39 +01:00
the-djmaze
6e6ed0eab3 Resolve #974 2023-02-17 09:11:48 +01:00
the-djmaze
3b7deb1328 Improved cleanHtml() and cleanCSS() handling 2023-02-17 08:54:17 +01:00
the-djmaze
1506a158f4 Show loadavg in Admin -> About for #971 2023-02-17 08:26:53 +01:00
the-djmaze
5b536f81e3 Resolve #971 2023-02-16 22:30:55 +01:00
the-djmaze
a9eff27f99 Resolve #972 2023-02-16 16:08:05 +01:00
the-djmaze
e4e12bdaa9 Cleanup fix for #962 2023-02-16 14:45:12 +01:00
the-djmaze
b240c26667 Resolve #962 2023-02-16 14:02:55 +01:00
the-djmaze
5216535278 Move the "upload EML file to mailbox" Append feature into a DoFolderAppend() 2023-02-16 13:13:23 +01:00
the-djmaze
fb2eb62f83 Rename Folder Hash to ETag as it is the more obvious name 2023-02-16 10:55:23 +01:00
the-djmaze
1a05a0adf2 Some $this->MailClient() to $this->ImapClient() 2023-02-16 09:01:14 +01:00
the-djmaze
d98a2e015e Released new Avatars extension 2023-02-16 09:00:00 +01:00
the-djmaze
99b73d87f6 Check login handling for #969 2023-02-16 08:40:28 +01:00
the-djmaze
90d039b36b Bugfix: DoMessageList() hash check was incorrect 2023-02-16 08:38:41 +01:00
the-djmaze
38c4b4e26e Resolve #969 2023-02-15 23:04:23 +01:00
cm-schl
9cfe164633 Works now with additional accounts:
The mail address is correctly set by ldap value.
But plugin needs rework: it makes no sense to let the user use the non
existing mail address "username@domain" used
only by SnappyMail. Plugin should always use a looked up mail address
2023-02-15 17:57:59 +01:00
the-djmaze
8c97b64a55 Fix translations 2023-02-15 17:33:06 +01:00
the-djmaze
4852895488 Resolve #967 btoa() issue 2023-02-15 17:29:47 +01:00
the-djmaze
03379a6163 Moved AllowDraftAutosave option from Settings -> Security to Settings -> General -> Compose 2023-02-15 17:05:53 +01:00
the-djmaze
edcb84b4ae SmtpClient->Connect() use $oSettings->Ehlo 2023-02-15 12:59:49 +01:00
cm-schl
aa03e84c79
Merge branch 'the-djmaze:master' into rework-on-mailaddress-handling 2023-02-15 12:26:35 +01:00
the-djmaze
353fdda3e5 Move some message checks to MailSo\Mime\Message 2023-02-15 12:03:51 +01:00
the-djmaze
d629f42e4c x-script not used anymore 2023-02-15 12:03:21 +01:00
the-djmaze
ae1e6184c3 Resolve #956 2023-02-15 11:57:30 +01:00
the-djmaze
7a762e50c7 Resolve #957 2023-02-15 11:57:01 +01:00
the-djmaze
8058c8d2f6 Allow <body> element CSS as .mail-body 2023-02-15 11:26:21 +01:00
the-djmaze
d11118a159 Resolve #961 2023-02-15 10:54:12 +01:00
the-djmaze
f970e6be2d Merge branch 'master' of github.com:the-djmaze/snappymail 2023-02-15 10:37:56 +01:00
the-djmaze
163eb51ff6 Resolve #964 2023-02-15 10:37:41 +01:00
the-djmaze
b18f0ee2d9
Merge pull request #959 from hguilbert/patch-2
Update user.json
2023-02-14 20:30:25 +01:00
the-djmaze
d58cb75e53 Resolve #958 2023-02-14 20:12:16 +01:00
the-djmaze
9dd917894d Resolve #960 2023-02-14 19:04:14 +01:00
hguilbert
305c3b6a86
Update user.json
Some translation ...
2023-02-14 18:24:25 +01:00
the-djmaze
bd9e50f222 Bugfix: new CSS <style> removing (background-)?color failed 2023-02-14 16:51:51 +01:00
the-djmaze
492f1d4b10 v2.26.1 2023-02-14 15:32:12 +01:00
the-djmaze
5105f6d4c1 Improved handling of secure message <style> CSS class names 2023-02-14 15:21:43 +01:00
the-djmaze
64818025e8 Added feature to allow <style> in messages 2023-02-14 14:54:38 +01:00
the-djmaze
aae3072209 Prepare cleanHtml() for new feature 2023-02-14 14:07:38 +01:00
the-djmaze
4d2cd82d95 Resolve #937 2023-02-14 12:30:11 +01:00
the-djmaze
f99daeebe5 Bugfix: message() is null 2023-02-14 12:26:31 +01:00
the-djmaze
1f86b73414 Bugfix: OpenPGP decrypted attachments friendlySize not a function #89 2023-02-14 11:37:08 +01:00
the-djmaze
e2391d1ad2 Idea code for #951 2023-02-14 10:21:26 +01:00
the-djmaze
614e6c5756 Don't clone message for viewing, so that there is instant interaction with messagelist item 2023-02-14 10:11:25 +01:00
the-djmaze
b7a68a4245 Fix message @media print 2023-02-14 09:17:53 +01:00
the-djmaze
a696d6c0a1 Bugfix: addressparser() didn't handle groups properly 2023-02-13 19:47:24 +01:00
the-djmaze
ebf429215b view Message in new tab/window was missing BCC 2023-02-13 19:46:30 +01:00
the-djmaze
d898178985 emailArrayToStringLineHelper() filter addresses without email address
and simplify ComposeType handling
2023-02-13 19:19:00 +01:00
the-djmaze
974acaab90 Cleanup EmailModel 2023-02-13 17:05:44 +01:00
the-djmaze
ceac15ace9 Bugfix: middleclick failed when messagesDom not yet initialized 2023-02-13 16:36:38 +01:00
the-djmaze
33653eae81 Improved email address parsing and handling 2023-02-13 16:15:26 +01:00
the-djmaze
e8c93a1d0c Improved EmailModel handling 2023-02-12 18:28:30 +01:00
the-djmaze
57dc1d29b3 Update Identity class to latest PHP features 2023-02-12 18:27:55 +01:00
the-djmaze
396a1a423e Resolve #936 2023-02-12 18:27:06 +01:00
the-djmaze
477628ed70 #950 always return EmailCollection 2023-02-12 01:20:20 +01:00
the-djmaze
dbabbb9385
bugfix for solution #950 incorrect mailto link 2023-02-12 01:13:30 +01:00
the-djmaze
6237a3691b Resolve #950 2023-02-12 01:02:55 +01:00
the-djmaze
98d686bc14 Cleanup AbstractViewPopup handling 2023-02-12 00:27:11 +01:00
the-djmaze
163e926d6b
Merge pull request #948 from lota/patch-4
Italian translation fix - password again
2023-02-11 22:40:35 +01:00
the-djmaze
199fb1a8d6
Merge pull request #947 from lota/patch-3
Password italian translation
2023-02-11 22:40:25 +01:00
the-djmaze
d26fb2db05 improved blockquote switcher design 2023-02-11 20:42:13 +01:00
the-djmaze
e4b0d43b3d Bugfix: screenOnRoute sometimes failed 2023-02-11 20:41:54 +01:00
the-djmaze
415102aa37 Replace Element.fromHTML() with createElement() 2023-02-11 20:38:57 +01:00
the-djmaze
820b26a315 CSS don't display:flex when [hidden] 2023-02-11 20:00:40 +01:00
Lota Bi
4c18b84455
Italian translation fix - password again
sorry, PASSWORD lemma duplicate in admin.json too
for my opinion password is a common word that not require translation in italian
2023-02-11 18:17:40 +01:00
Lota Bi
ff53f8f8fe
Password italian traslation
Password not need translation
2023-02-11 18:14:22 +01:00
the-djmaze
8b02d7f892 Improved fix for #941 and #944 2023-02-10 17:31:10 +01:00
the-djmaze
7dbcc3a58d
Merge pull request #944 from e-foundation/master
fix default theme selection
2023-02-10 16:17:43 +01:00
Nivesh Krishna
0c6950d49c
fix default theme selection 2023-02-10 20:02:17 +05:30
cm-schl
424c1ff90a Prepared ldap query to optionally read mail addresses
for main account and additional accounts
2023-02-10 12:04:50 +01:00
the-djmaze
399ae06a5c v2.26.0 2023-02-10 11:38:41 +01:00
cm-schl
30e3a8cc0f added new config options to overwrite the mail address 2023-02-10 11:11:24 +01:00
the-djmaze
2f22267a77 Resolve #836 2023-02-10 11:06:12 +01:00
the-djmaze
67e359e616 Merge branch 'master' of github.com:the-djmaze/snappymail 2023-02-10 09:56:06 +01:00
the-djmaze
31dd1b74b8 Resolve #938 2023-02-10 09:55:52 +01:00
the-djmaze
3655639bda
Merge pull request #942 from BenAlanSouthall/patch-1
Add Integrations with Apps to Documentation for Nextcloud
2023-02-10 07:47:04 +01:00
Ben Southall (Work Account)
916501c3ff
Add Integrations with Apps to Documentation for Nextcloud
Just worked out all we needed to do to get all users to come through was enable the contacts app. Nice!!! would be good to have both mention and any specific details of [these features](https://apps.nextcloud.com/apps/snappymail) in the docs :) :
> Integration with other Nextcloud apps (Contacts, Files and Calendar)
2023-02-10 02:01:34 +00:00
the-djmaze
f05ff65b30 Change for Chrome caching issue #188 2023-02-09 23:15:24 +01:00
the-djmaze
9d02be6a20 MAke settings panel 100% wide 2023-02-09 20:54:18 +01:00
the-djmaze
298c0ac866 Resolve #941 2023-02-09 20:53:52 +01:00
the-djmaze
1fb3a26bd7 Files 0755 to 0644 2023-02-09 16:53:05 +01:00
the-djmaze
f1640d7565 Some spacing between message "view images" buttons for #201 2023-02-09 16:52:40 +01:00
the-djmaze
b2079f7263 Bugfix: whitelist fails when empty or when using : #938 2023-02-09 16:16:50 +01:00
the-djmaze
6c14055006 Resolve #939 2023-02-09 13:52:41 +01:00
the-djmaze
2b7962805f PdoAddressBook ORDER BY deleted DESC to prevent sync and export issues 2023-02-09 13:50:09 +01:00
the-djmaze
08d4cf3648 Bugfix: upgrade Undefined variable $items 2023-02-09 12:57:00 +01:00
the-djmaze
e364b6286f Get account local sqlite AddressBook working (but still inactive) 2023-02-09 12:54:36 +01:00
the-djmaze
8d6eb96c8f v2.25.5 2023-02-09 10:37:43 +01:00
the-djmaze
9466fc71a8 Reduce memory usage on addressbook vcf import 2023-02-09 10:12:13 +01:00
the-djmaze
e6bdb37ca1 Kolab error: $id is string, should be int 2023-02-09 09:29:45 +01:00
the-djmaze
4145c87e55 Resolve #931 2023-02-09 09:29:19 +01:00
the-djmaze
58e1c2e535 Bugfix: default preview-pane layout width/height 2023-02-09 09:02:30 +01:00
the-djmaze
62fb32a914
Merge pull request #930 from cm-schl/patch-1
Update index.php
2023-02-08 18:12:27 +01:00
the-djmaze
b448b8661d Properly fix #928 by redesign using display: flex 2023-02-08 17:39:30 +01:00
cm-schl
a24dade801
Update index.php 2023-02-08 15:38:47 +01:00
the-djmaze
8b8b0fc7e4 small cleanup 2023-02-08 12:12:02 +01:00
the-djmaze
f35ea9a815 Workaround #928 2023-02-08 12:05:38 +01:00
the-djmaze
85d3a95dec Message whitelist #201:
* sort options based on domain count (highest top)
* hide when whitelist not active
2023-02-08 11:39:07 +01:00
the-djmaze
e12eeeafc6 Resolve #927 Call to undefined method MailboxDetectPlugin::Logger() 2023-02-08 10:43:11 +01:00
the-djmaze
4ef78c24e1 Log more info for #926 2023-02-08 10:31:22 +01:00
the-djmaze
7da5e84a35
Merge pull request #925 from Digital-Dimensions-LLC/master
Add Dark Themes
2023-02-07 17:17:05 +01:00
Justyn Freeman
cd6fc89e38 Add BlackWood theme 2023-02-07 10:36:55 -05:00
Justyn Freeman
919acff536
Merge branch 'the-djmaze:master' into master 2023-02-07 10:33:47 -05:00
Justyn Freeman
a5be8b9afc Update styles.css 2023-02-07 10:33:17 -05:00
Justyn Freeman
12c95159f2 Update styles.css 2023-02-07 10:33:14 -05:00
Justyn Freeman
13d80b41a6 Update styles.css 2023-02-07 10:33:10 -05:00
the-djmaze
831410e8f0 Whitelist images dropdown #201 2023-02-07 15:48:39 +01:00
the-djmaze
05d8e7b353 Improved autocrypt #342 2023-02-07 15:25:28 +01:00
the-djmaze
636ffe5fbb Add more logging for #907 2023-02-07 14:50:06 +01:00
the-djmaze
f2ca5f6a30 Probably solution for #907 2023-02-07 11:49:05 +01:00
the-djmaze
7286134382 Bugfix: inline images sometimes failed 2023-02-07 10:50:40 +01:00
the-djmaze
276cbc2f03 On upgrade set umask and chmod files for #358 2023-02-07 10:33:23 +01:00
the-djmaze
69064e2ad0 Image whitelist shortcuts as discussed in #201 2023-02-07 10:23:10 +01:00
the-djmaze
7aa5142047 Speedup cleanHtml() parser 2023-02-07 08:56:51 +01:00
the-djmaze
a1441c9f22 Removed disabled blockquote height calculator for #902 2023-02-07 08:51:02 +01:00
the-djmaze
e8669a20bb Resolve #918 2023-02-07 08:31:52 +01:00
the-djmaze
cb7a1fe30e
Resolve #921 2023-02-06 23:46:35 +01:00
the-djmaze
0c7593fa0e
Merge pull request #919 from hguilbert/patch-1
Update user.json
2023-02-06 23:30:57 +01:00
the-djmaze
cfeeeb3cd4
Merge pull request #920 from ner00/master
Translate IMAGES_OPTION_DKIM
2023-02-06 23:30:37 +01:00
Justyn Freeman
634a8ee9c0 Update styles.css 2023-02-06 14:35:58 -05:00
Justyn Freeman
dc2ebdf8e2 Update styles.css 2023-02-06 14:35:53 -05:00
Justyn Freeman
32f749bcb1 Update styles.css 2023-02-06 14:35:49 -05:00
ner0
336e2e756f Translate IMAGES_OPTION_DKIM 2023-02-06 18:56:43 +00:00
hguilbert
be47787166
Update user.json
Update translations - Typo correction
2023-02-06 17:54:27 +01:00
the-djmaze
e8f200186d Added external images option 'Always when DKIM is valid' for #201 2023-02-06 17:16:31 +01:00
Justyn Freeman
a898b2f405 Update styles.css
Fix tiling background image (mostly)
2023-02-06 10:40:47 -05:00
the-djmaze
2a619dfa93
Merge pull request #917 from ner00/master
Update and tweak pt/pt-PT translation
2023-02-06 16:40:28 +01:00
Justyn Freeman
b4625f1599 Update styles.css
Fix tiling background image (mostly)
2023-02-06 10:40:18 -05:00
ner0
9cc94287f9
Merge branch 'the-djmaze:master' into master 2023-02-06 15:35:07 +00:00
ner0
4d04094baa Update and tweak pt/pt-PT translation 2023-02-06 15:34:16 +00:00
Justyn Freeman
e8938ebde7 Add StripesDark
Add dark variation of Stripes theme
2023-02-06 10:21:27 -05:00
Justyn Freeman
86918738e4 Add NightShine
Add dark mode variation of Shine theme
2023-02-06 10:19:09 -05:00
Justyn Freeman
81a04ef861 Add BlurredDark
Add dark variation of Blurred theme
2023-02-06 10:18:49 -05:00
the-djmaze
8af68d7c65 Prevent Apache access error due to directories being 0700 2023-02-06 15:47:26 +01:00
the-djmaze
67b13e5b1b Admin -> Config layout fix 2023-02-06 15:41:30 +01:00
the-djmaze
88bfa5663f Prevent loading loop between MessageList and FolderInformation requests 2023-02-06 13:54:03 +01:00
the-djmaze
9cec77ecc1 Small messageList layout improvement 2023-02-06 13:25:20 +01:00
the-djmaze
4f7f713046 v2.25.4 2023-02-06 11:51:41 +01:00
the-djmaze
7c090ee775 Resolve #916 2023-02-06 11:28:15 +01:00
the-djmaze
a6e8e5cb82 Disable blockquotes height calculation for #902 2023-02-06 11:14:53 +01:00
the-djmaze
e65dcc1d79 Some style change for settings panels 2023-02-06 11:05:52 +01:00
the-djmaze
53569957fe Resolve #897 2023-02-06 10:33:12 +01:00
the-djmaze
b2fdc39670 Cleanup material-design checkbox 2023-02-06 10:04:00 +01:00
the-djmaze
7ffe02c28a Revert #897 2023-02-06 09:53:00 +01:00
the-djmaze
f01c6bce4b Fix some MailMessageView styling 2023-02-06 09:49:49 +01:00
the-djmaze
70e32e0905 Bugfix: autofocus was triggered when view resized 2023-02-06 08:49:56 +01:00
the-djmaze
cc054054c2 Merge branch 'master' of github.com:the-djmaze/snappymail 2023-02-05 19:46:09 +01:00
the-djmaze
8648b3af88 Resolve #912 2023-02-05 19:45:56 +01:00
the-djmaze
93ff3001c6 Hopefull improved styling for Firefox mobile fixes 2023-02-05 19:45:03 +01:00
the-djmaze
07409cde50 Set CSS color-scheme: dark; 2023-02-05 19:22:32 +01:00
the-djmaze
7bdc6c683b
Resolve #915 due to #898 2023-02-05 14:30:53 +01:00
the-djmaze
e2266d1ffd Add CSS color-scheme for #913 2023-02-05 12:29:19 +01:00
the-djmaze
7b586cf5fb
Merge pull request #913 from Digital-Dimensions-LLC/master
Add LoveDark Theme
2023-02-05 12:25:57 +01:00
the-djmaze
af6f83b193
Merge pull request #914 from hguilbert/patch-1
Update user.json
2023-02-05 10:15:39 +01:00
hguilbert
e464f253da
Update user.json
Some translations in "Français"
2023-02-05 08:34:22 +01:00
Justyn Freeman
5452d158f2 Re-Upload assets 2023-02-05 02:25:57 -05:00
Justyn Freeman
0437e11a47
Delete preview[1].png 2023-02-05 02:22:39 -05:00
Justyn Freeman
2e68dd6bef
Delete background[1].jpg 2023-02-05 02:22:28 -05:00
Justyn Freeman
7b9993b84f
Import image assets 2023-02-05 02:15:33 -05:00
Justyn Freeman
674859b835
Create styles.css 2023-02-05 02:13:09 -05:00
the-djmaze
dd829c5d97 $bUrlEncode always false, so remove it 2023-02-04 21:27:30 +01:00
the-djmaze
be0348b324 Resolve #910 MailSo\Base\Http::GetHost(): Return value must be of type string 2023-02-04 21:20:03 +01:00
the-djmaze
fc5b26dfc2 Added message maximum quoted text level for speed.
Read https://github.com/the-djmaze/snappymail/issues/902#issuecomment-1416777217
2023-02-04 16:39:50 +01:00
the-djmaze
b0a4fb77be Bugfix for whitelist to show images directly #201 2023-02-04 15:44:51 +01:00
the-djmaze
539ddcc51f Moved pluginEnable() to \SnappyMail\Repository::enablePackage() 2023-02-04 15:28:46 +01:00
the-djmaze
cf08c1aba4 Only allow from CLI #907 2023-02-04 15:08:42 +01:00
the-djmaze
7671503562 Example script for #907 2023-02-04 14:57:31 +01:00
the-djmaze
22ffe4a079 Bugfix: btn-thin were too high 2023-02-03 20:23:42 +01:00
the-djmaze
a0a9d8fd65 Solve messagelist thread issue as mentioned at
https://github.com/the-djmaze/snappymail/issues/445#issuecomment-1415949559
2023-02-03 15:51:41 +01:00
the-djmaze
8c676aa267 v2.25.3 2023-02-03 15:15:49 +01:00
the-djmaze
426c92fe33 Make collapse quotes optional for #902 2023-02-03 14:40:56 +01:00
the-djmaze
b859cd6aec Improved loginErrorDelay to prevent timing attacks 2023-02-03 14:36:13 +01:00
the-djmaze
c4df524a06 Prevent decrypt error for #859 2023-02-03 12:30:46 +01:00
the-djmaze
b182372b83 Speedup for #902 2023-02-03 12:12:40 +01:00
the-djmaze
c6db5f1def Move blockquote switcher to HTML parser #902 2023-02-03 11:17:16 +01:00
the-djmaze
39bb3719c2 Moved some application.ini settings 2023-02-03 11:16:15 +01:00
the-djmaze
16daa7b55e Resolve #891 and other index cache issues 2023-02-03 09:20:42 +01:00
the-djmaze
11c5b0e8f1 Resolve bug in images whitelist #201 2023-02-03 08:39:09 +01:00
the-djmaze
05f45d36d1 Resolve #901 2023-02-03 07:42:17 +01:00
the-djmaze
722bec1a1a Resolve #900 2023-02-02 22:06:43 +01:00
the-djmaze
489ed81c84 Move "assets" to proper location in snappymail/v/0.0.0/static/ 2023-02-02 17:19:06 +01:00
the-djmaze
4521a9917a Also allow changing email address (untested) for #859 2023-02-02 15:09:03 +01:00
the-djmaze
73e6848ff4 Don't remember GnuPGP key passphrase when it fails #840 2023-02-02 14:50:46 +01:00
the-djmaze
e01af6a7ac Set checkMailInterval to 15 minutes by default (now that it is configurable) 2023-02-02 14:43:22 +01:00
the-djmaze
acc1c0badc Don't remember OpenPGP key passphrase when it fails #840 2023-02-02 14:04:37 +01:00
the-djmaze
78938caaf3 v2.25.2 2023-02-02 13:26:23 +01:00
the-djmaze
565ef41eba Comments for #537 2023-02-02 12:55:06 +01:00
the-djmaze
a52881fb67 Resolve #840 2023-02-02 12:39:21 +01:00
the-djmaze
af3cecb2e1 https://github.com/the-djmaze/snappymail/issues/895#issuecomment-1412891229 2023-02-02 09:58:09 +01:00
the-djmaze
a7081660f0 Resolve #898 2023-02-02 09:51:00 +01:00
the-djmaze
8cf8823d56 Nextcloud use RainLoop settings #898 2023-02-01 21:09:18 +01:00
the-djmaze
094609102d Avatars use current app font #888 2023-02-01 13:02:37 +01:00
the-djmaze
cefe679c98 Resolve #486 2023-02-01 12:21:31 +01:00
the-djmaze
67488a56f6 https://github.com/the-djmaze/snappymail/issues/569#issuecomment-1411330109 2023-02-01 11:19:14 +01:00
the-djmaze
5259da41f2 Details for #188 2023-02-01 11:09:18 +01:00
the-djmaze
7236412e48
Merge pull request #896 from master3395/master
Update admin.json
2023-02-01 10:23:28 +01:00
the-djmaze
d68843e4ec Nextcloud prevent serving precompressed files due to too many setups that fail 2023-02-01 10:22:46 +01:00
the-djmaze
cc383f6b0e Resolve #233 2023-02-01 10:01:13 +01:00
Master3395
b5c6ecac3e Update admin.json
Updating translations for nb-NO
2023-01-31 21:33:24 +01:00
the-djmaze
76ae57b481 Resolve #887 2023-01-31 17:38:38 +01:00
the-djmaze
71eaaf007e Resolve #828 (floating toolbar not needed anymore) 2023-01-31 15:40:07 +01:00
the-djmaze
516a080a5b Remove unused translation 2023-01-31 15:39:44 +01:00
the-djmaze
2b8fbaf091 Prevent empty area scroll on body 2023-01-31 14:46:14 +01:00
the-djmaze
07079b519a Resolve #201 2023-01-31 14:45:50 +01:00
the-djmaze
6129fb4233 Paste image makes it max 1024px width/height #262 2023-01-31 13:13:29 +01:00
the-djmaze
9f5c75d9db Remove unused AUTH_SPEC_LOGOUT_TOKEN_KEY and AUTH_SPEC_LOGOUT_CUSTOM_MSG_KEY 2023-01-31 12:47:57 +01:00
the-djmaze
7578d18cab cleanup folderInformation() request 2023-01-31 12:47:21 +01:00
the-djmaze
a9764bf034 \RainLoop\Utils::SetCookie() to \SnappyMail\Cookies::set() 2023-01-31 12:46:37 +01:00
the-djmaze
131be583e5
Merge pull request #894 from e-foundation/nc-plugin-bump
bump NC plugin
2023-01-31 11:38:46 +01:00
the-djmaze
dc49aaf94d Resolve #849 2023-01-31 11:35:25 +01:00
the-djmaze
d7b5f3f4db Also protect GET requests with token for #892 2023-01-31 11:30:24 +01:00
Nivesh Krishna
206758c803
bump NC plugin 2023-01-31 15:26:32 +05:30
the-djmaze
b1c9bebce6 Force token for #892 2023-01-31 10:49:06 +01:00
the-djmaze
977dc1e5e4 Move and improve cookie handling so that it might catch #537 2023-01-31 10:27:23 +01:00
the-djmaze
fa3d4727f9 Also support SnappyMail\TAR on core update 2023-01-31 09:31:01 +01:00
the-djmaze
b1471c2bbd SnappyMail\TAR::extractTo support the $files and $overwrite parameters 2023-01-31 09:27:11 +01:00
the-djmaze
6e2e07ce98 Merge \MailSo\Base\Utils::RecRmDir() and \MailSo\Base\Utils::RecTimeDirRemove() 2023-01-31 09:05:27 +01:00
the-djmaze
a86c143160 Resolve #893 2023-01-31 09:03:01 +01:00
the-djmaze
3396a3d791 Protect against multiple accounts on different tabs #892 2023-01-30 22:42:37 +01:00
the-djmaze
c21817e5e0 v2.25.1 2023-01-30 15:49:58 +01:00
the-djmaze
e23e0138a9 Enable RFC 8689 2023-01-30 15:21:17 +01:00
the-djmaze
1126712d11 Improve create folder popup 2023-01-30 14:26:42 +01:00
the-djmaze
bc196bcd1a Added missing knockoutjs checked.js binding 2023-01-30 14:16:10 +01:00
the-djmaze
08a082a190 Merge branch 'material-design-checkbox-accessibility' 2023-01-30 14:14:26 +01:00
the-djmaze
93d1b6de9a unused typeToBitArray 2023-01-30 14:04:08 +01:00
the-djmaze
ac424fc584 re-enable simplified knockoutjs checked binding 2023-01-30 14:03:45 +01:00
Rene Hampölz
96e090650d fix: Clipped elements on disabled left panel 2023-01-30 11:18:46 +00:00
Rene Hampölz
15deadaa1c fix: Broken styles in the latest snappymail release 2023-01-30 11:12:11 +00:00
the-djmaze
d0376b53a8 Improved accessibility of material-design checkbox 2023-01-30 10:09:52 +01:00
the-djmaze
e148cdea75 Always subscribe to new folder by default 2023-01-30 09:12:11 +01:00
the-djmaze
4c8d84d07b Merge NoScript, NoCookie and BadBrowser pages in Index.html 2023-01-30 09:10:49 +01:00
the-djmaze
860b7701f3 Resolve #885 2023-01-29 18:28:35 +01:00
the-djmaze
6666712bc9 Cleanup #884 2023-01-28 08:28:40 +01:00
the-djmaze
f862634143
change for #884 2023-01-28 00:47:57 +01:00
the-djmaze
e45959949c
changes for #884 2023-01-28 00:44:10 +01:00
the-djmaze
c6500dfe14
Update Account.php
Bugfix send message using PHP mail() failed
2023-01-28 00:09:56 +01:00
the-djmaze
352ef98bc6 Resolve #849 2023-01-27 21:29:08 +01:00
the-djmaze
49a63f37c9 Resolve #880 2023-01-27 16:08:42 +01:00
the-djmaze
32e933b435 PHP < 8.1 ini_set() only accepts strings when declare(strict_types=1); is used 2023-01-27 16:07:05 +01:00
the-djmaze
2750e0698d Cleanup translations.
Also see https://snappymail.eu/translate.php
2023-01-27 12:15:25 +01:00
the-djmaze
f929c8fb69 Resolve #875 2023-01-27 09:19:32 +01:00
the-djmaze
f79e6f253d Improve mobile view left side background color 2023-01-27 08:42:18 +01:00
the-djmaze
f6f3040ad2 Also fix settings on upgrade inside Nextcloud 2023-01-27 08:30:34 +01:00
the-djmaze
82a1f4d781 Make #879 optional 2023-01-26 22:53:12 +01:00
the-djmaze
dc6674af4a
Merge pull request #879 from e-foundation/fix-contacts-suggestions-nextcloud
Add check to unregister system addressbook
2023-01-26 21:53:44 +01:00
the-djmaze
0b421d5a62 Nextcloud /themes/*/snappymail/(style.css|preview.png) for #875 2023-01-26 21:42:14 +01:00
Akhil
6dff9cd781
Add check to unregister system addressbook
Signed-off-by: Akhil <akhil.potukuchi@gmail.com>
2023-01-27 01:32:00 +05:30
the-djmaze
3b43a8bc78 Resolve #869 2023-01-26 20:03:07 +01:00
the-djmaze
d65d77ef2f Idea for #875 2023-01-26 20:02:48 +01:00
the-djmaze
188b419910 v2.25.0 2023-01-26 16:35:54 +01:00
the-djmaze
5b1404ffac Resolve #878 2023-01-26 15:53:00 +01:00
the-djmaze
271065fe44 Fix mobile view issues 2023-01-26 15:37:27 +01:00
the-djmaze
8047a36b70 #rl-content to use flex for better layout control 2023-01-26 14:11:30 +01:00
the-djmaze
cfcc8c73af Decode some Copernica link tracking 2023-01-26 10:49:57 +01:00
the-djmaze
974350ebee Found more JSON properties to change into JavaScript camelCase 2023-01-26 10:41:55 +01:00
the-djmaze
b9ef8ae2c9 Resolve #866 2023-01-25 10:24:18 +01:00
the-djmaze
ba543c95d7 Improved #871 2023-01-25 09:52:30 +01:00
the-djmaze
09f00d1766
Merge pull request #871 from codiflow/master
Added right margin to folder search input wrapper
2023-01-25 09:49:53 +01:00
the-djmaze
528b6cd096 cleanup some json code 2023-01-25 09:41:15 +01:00
the-djmaze
f23b7205c6 Squire Colour to Color 2023-01-25 09:28:48 +01:00
the-djmaze
4cfd6479ce Better Sieve rainloop.user script handling 2023-01-25 09:28:09 +01:00
the-djmaze
f080a302b1 Almost all JSON properties to JavaScript camelCase 2023-01-24 18:58:25 +01:00
Christian
d6bde1aab0
Added right margin to folder search input wrapper 2023-01-23 13:55:46 +01:00
the-djmaze
7a53cae32f Remove unused function ContentTypeType() due to previous commit 2023-01-22 22:27:40 +01:00
the-djmaze
7e42e99019 Remove some unused functions 2023-01-22 22:24:27 +01:00
the-djmaze
87df281434 Improve MailSo\Imap\BodyStructure 2023-01-22 20:06:52 +01:00
the-djmaze
3d63eeb79e Cleanup MIME part FileName handling 2023-01-22 16:07:49 +01:00
the-djmaze
65bf9be580 Maybe solution for #867 2023-01-22 13:32:03 +01:00
the-djmaze
da4b9684b4 Log RAW request for #867 2023-01-22 10:21:03 +01:00
the-djmaze
c30c41a79b Nextcloud _htaccess to .htaccess failed 2023-01-22 10:20:13 +01:00
the-djmaze
974adb78e2 Start documenting JavaScript rl object as noted in #865 2023-01-19 20:09:52 +01:00
the-djmaze
95024aca10 Bugfix: Nextcloud ?admin wrong layout 2023-01-19 19:41:30 +01:00
the-djmaze
d43d3340e4 Fix default messageListItem border left color for dark mode 2023-01-19 19:12:11 +01:00
the-djmaze
e1d66d90ae Resolve #863 2023-01-19 17:27:14 +01:00
the-djmaze
d25af53dfb v2.24.6 2023-01-19 17:11:40 +01:00
the-djmaze
79510eb79c Resolve #864 2023-01-19 16:46:39 +01:00
the-djmaze
dba125ad59 Change for #864 2023-01-19 15:49:49 +01:00
the-djmaze
7feb9a9f41 ImapConnectAndLoginHelper() to ImapConnectAndLogin() 2023-01-19 10:55:34 +01:00
the-djmaze
b3a969fb8a Bugfix and improved 2023-01-19 10:55:01 +01:00
the-djmaze
890fe132e1 Remove unused $bUsePhpMail 2023-01-19 10:53:35 +01:00
the-djmaze
790edc73c8 Workaround #813 2023-01-18 18:50:23 +01:00
the-djmaze
53eac88edb Move some Nextcloud SnappyMailHelper code to new InstallStep 2023-01-18 12:18:10 +01:00
the-djmaze
b20df3209f Fix language SETTINGS_LABELS 2023-01-18 11:45:27 +01:00
the-djmaze
1d4b20bb79 Prepare RFC 8689 2023-01-18 08:56:56 +01:00
the-djmaze
bd35521f41 Resolve #525 and #855 2023-01-18 08:44:24 +01:00
the-djmaze
7f8707c4c0 Cleanup messages cache handling 2023-01-18 08:38:26 +01:00
the-djmaze
745a3603fe Resolve #852 2023-01-18 08:20:51 +01:00
the-djmaze
dcd1d1cbd4 Resolve unknown CSP directive 'strict-dynamic' in Safari 13.1.2 2023-01-17 15:49:35 +01:00
the-djmaze
b2ca7f50ad Resolve timeStyle issue found in Safari 13.1.2 2023-01-17 15:48:48 +01:00
the-djmaze
cc068e9dc4 Small wrapping fix for #843 2023-01-17 15:09:48 +01:00
the-djmaze
98996ba4b7 Resolve #843 2023-01-17 14:26:00 +01:00
the-djmaze
d41f9d8eee Improved contacts suggestions limit handling #849 2023-01-17 12:31:07 +01:00
the-djmaze
51f250cc78 Plugins still use ImapConnectAndLoginHelper 2023-01-17 12:26:12 +01:00
the-djmaze
302dcf67c2 Bugfix: matchAnyRule() Return value must be of type bool, none returned 2023-01-17 12:19:45 +01:00
the-djmaze
80eea23b10 Add admin setting for the contacts suggestions limit
Improves visual info for #849
2023-01-17 11:52:07 +01:00
the-djmaze
c5c180cb2e Move createDomain & createDomainAlias buttons below table 2023-01-17 11:22:30 +01:00
the-djmaze
29beb9b75d Improved visibility Admin Domain Test errors 2023-01-17 11:02:45 +01:00
the-djmaze
61899ce092 Prepare for custom SMTP credentials for #859 #458 #431 #233 2023-01-17 10:28:19 +01:00
the-djmaze
038e093e68 Merge branch 'master' of github.com:the-djmaze/snappymail 2023-01-16 14:11:08 +01:00
the-djmaze
ad8eccf83b Resolve #858 2023-01-16 14:10:55 +01:00
the-djmaze
a72b671b1c
Merge pull request #854 from dominiquefournier/patch-6
Update fr-FR translations
2023-01-15 08:56:50 +01:00
dominiquefournier
e1f8fa5090
Update fr-FR translations 2023-01-14 19:14:04 +01:00
the-djmaze
177dfcb7e5 Improved and solve #847 2023-01-12 16:54:40 +01:00
the-djmaze
e1aa566ec8
Merge pull request #847 from SWEETGOOD-DE/master
Adding a button to clear folder search input field
2023-01-12 16:43:24 +01:00
codiflow
bd35a73f5f Added search input wrapper div 2023-01-12 15:52:54 +01:00
the-djmaze
686fa20945 v2.24.5 2023-01-12 15:16:32 +01:00
codiflow
7b29dca261 Adding a button to clear folder search input field 2023-01-12 13:24:34 +01:00
the-djmaze
a2336be273 Merge branch 'master' of github.com:the-djmaze/snappymail 2023-01-12 08:58:33 +01:00
the-djmaze
6923ddebe8 Bugfix: MessageCollection->totalEmails was incorrect on search 2023-01-12 08:58:11 +01:00
the-djmaze
c9608f18a2
Merge pull request #846 from cm-schl/master
Improve german and italian translation
2023-01-11 20:55:18 +01:00
cm-schl
f10f93fd8f Merge branch 'master' of https://github.com/cm-schl/snappymail 2023-01-11 16:48:14 +01:00
cm-schl
5eab70f935 Improve italian translation 2023-01-11 16:46:17 +01:00
cm-schl
2321abcf47 Improve german translation 2023-01-11 16:46:05 +01:00
the-djmaze
07dae9dd30 Resolve ReCaptcha CSP #841 2023-01-11 08:43:34 +01:00
the-djmaze
9c9eea0dac Bugfix: auth_logging failed #489 2023-01-10 09:10:20 +01:00
the-djmaze
4572d3f43d Bugfix: undefined $sValue 2023-01-09 21:21:59 +01:00
the-djmaze
5149357888 Bugfix: Class "RainLoop\Actions\Notifications" not found #839 2023-01-09 14:25:25 +01:00
the-djmaze
d3b716cfda I broke Remote.messageList() in previous commit 2023-01-09 14:06:33 +01:00
the-djmaze
be512a0333 Improved solution for #686 2023-01-09 13:11:27 +01:00
the-djmaze
cf12960507 Improved cache handling 2023-01-09 12:28:07 +01:00
the-djmaze
29854311f9 Don't use \ 2023-01-09 10:16:43 +01:00
the-djmaze
1311fd343b Improvements for #686 2023-01-05 16:27:13 +01:00
the-djmaze
9a5ad04a6e Avatars to 48x48 2023-01-05 12:29:25 +01:00
the-djmaze
712cfbb8f4 Put :login_name in change-password PDO default value 2023-01-05 12:18:15 +01:00
the-djmaze
7858b3e06a Improved avatars plugin with better cached image handling and custom images 2023-01-05 12:16:52 +01:00
the-djmaze
a2ddd493ce Improved MIME handling for JSON 2023-01-05 10:55:59 +01:00
the-djmaze
d4e1e90eef Better solution for #826 2023-01-05 09:55:09 +01:00
the-djmaze
d6d9be0dab Improve #826 2023-01-04 18:46:04 +01:00
the-djmaze
e95b919c1e Resolve #825 2023-01-04 16:11:10 +01:00
Rene Hampölz
4ead42aea2 fix: Center message subject 2023-01-04 11:39:51 +00:00
the-djmaze
02a553c79f Resolve #826 2023-01-03 20:56:38 +01:00
the-djmaze
a9c60137da Added untested KolabAddressBook autocreate contacts #812 2023-01-03 17:00:58 +01:00
the-djmaze
5ff8b28a12 Bugfix: Kolab AddressBook failed due to past changes 2023-01-03 16:22:57 +01:00
the-djmaze
e9504baa12 Bugfix: Typed property MailSo\Mail\MessageListParams::$sSearch must not be accessed before initialization 2023-01-03 16:11:20 +01:00
the-djmaze
1a71309b81 Drop confusing oldpass from default change-password query 2023-01-03 15:46:26 +01:00
the-djmaze
56c681bbd5 https://github.com/the-djmaze/snappymail/issues/724#issuecomment-1369826672 2023-01-03 15:25:24 +01:00
Rene Hampölz
c7991c91b7 chore: Improve simple attachment list 2023-01-03 14:12:33 +00:00
the-djmaze
19c0c5c98e Added DAV path error to log for #822 2023-01-03 15:11:17 +01:00
Rene Hampölz
2c3766f4d5 chore: Add group labels 2023-01-03 14:06:26 +00:00
the-djmaze
0e5ae38c58 More changes for #815 2023-01-03 14:15:09 +01:00
the-djmaze
cfd9c11db6 Moved sort options "FROM" direct below "DATE" for better understanding 2023-01-03 13:52:46 +01:00
the-djmaze
becdf29120 Changes for #815 2023-01-03 13:50:38 +01:00
the-djmaze
8bd466a656 Resolve #824 2023-01-03 13:26:08 +01:00
the-djmaze
1f9b3ec699 Fix Nextcloud .htaccess workaround 2023-01-03 13:26:00 +01:00
the-djmaze
8fdd0407db move release.php to cli/release.php 2023-01-03 13:20:27 +01:00
the-djmaze
1c9b53eb25 Remove 'set-version' argument for release.php 2023-01-03 12:58:38 +01:00
the-djmaze
de38c3b3e9 Added #821 2023-01-03 12:57:56 +01:00
the-djmaze
a88c2e94f9 Bugfix: forgot debug js/css setting change in Nextcloud 2023-01-03 12:26:01 +01:00
the-djmaze
5cadccb985 Bugfix: KolabAddressBook parse error 2023-01-03 10:38:07 +01:00
the-djmaze
083b6988ee Bugfix: Admin -> Contacts PDO test failed when using different AddressBookInterface 2023-01-03 10:37:28 +01:00
the-djmaze
27b19ef29d Resolve #823 2023-01-03 09:46:10 +01:00
the-djmaze
8b4019d934 Merge branch 'master' of github.com:the-djmaze/snappymail 2023-01-02 09:40:56 +01:00
the-djmaze
33d8482c4c New 'forward as attachment' glyph 2023-01-02 09:40:39 +01:00
the-djmaze
42c1c20a76 Resolve #787 2023-01-02 09:40:13 +01:00
the-djmaze
92d5a4176e
Merge pull request #820 from p-bo/patch-1
NC integration app metadata: add links to admin and dev docs
2023-01-01 22:45:20 +01:00
p-bo
1c43b21fa8
NC integration app metadata: add links to admin and dev docs 2023-01-01 19:02:22 +01:00
the-djmaze
e35d74be0b Add support for search criterias ON, SENTON, SENTSINCE and SENTBEFORE 2023-01-01 17:13:04 +01:00
the-djmaze
fb319c356d v2.24.4 2022-12-30 20:43:04 +01:00
the-djmaze
c8deecb4aa Speedup MessageListByRequestIndexOrUids() by using the new FetchIterate() 2022-12-30 19:47:40 +01:00
the-djmaze
fe718e81e9 Improved MessageList handling 2022-12-30 17:38:42 +01:00
the-djmaze
5eef0b409b Also check PHP_INT_SIZE if SnappyMail runs on 64bit 2022-12-30 15:34:31 +01:00
the-djmaze
4922586893 Cleanup config settings and provide some info 2022-12-30 15:11:25 +01:00
the-djmaze
4261bdafff Moved application.ini labs.use_app_debug_* to debug.* 2022-12-30 12:18:07 +01:00
the-djmaze
d1b31c6822 Improved sort when using message_list_limit in a virtual \All mailbox
https://github.com/the-djmaze/snappymail/issues/796#issuecomment-1367846187
2022-12-30 11:31:49 +01:00
the-djmaze
201d2e3c94 Translate GLOBAL/DEFAULT 2022-12-29 16:35:48 +01:00
the-djmaze
41db44bf35 Fix Dutch translation for confusing message (threads vs grouped) 2022-12-29 16:16:20 +01:00
the-djmaze
6abcbd16c4 Protect MessageListThreadsMap() 2022-12-29 16:15:38 +01:00
the-djmaze
34543d5bf3 On upgrade also update plugins in Nextcloud due to many misunderstandings and prevent invalid open issues 2022-12-29 14:42:11 +01:00
the-djmaze
94d33a6daf No need to check oCacher 2022-12-29 13:59:40 +01:00
the-djmaze
d4c4f84c13 https://github.com/the-djmaze/snappymail/issues/796#issuecomment-1367247159 2022-12-29 13:40:46 +01:00
the-djmaze
1cff18218e Bugfix #787 Typed property MailSo\Mail\MessageListParams::$bUseThreads must not be accessed before initialization 2022-12-29 11:03:43 +01:00
the-djmaze
ba460584bd https://github.com/the-djmaze/snappymail/issues/790#issuecomment-1366527884 2022-12-29 10:59:07 +01:00
the-djmaze
94c4b6c849 v2.24.3 2022-12-28 10:59:47 +01:00
the-djmaze
4805a377c4 Automatic update plugins at upgrade? 2022-12-28 10:56:11 +01:00
the-djmaze
3a96823506 When message list grouped by FROM show dynamic date/time 2022-12-28 10:55:37 +01:00
the-djmaze
ee27ef29d4 Bugfix: cPanel undefined $_ENV['SNAPPYMAIL_INCLUDE_AS_API'] 2022-12-28 10:54:52 +01:00
the-djmaze
7c928ee78e When sorting on FROM also sort on REVERSE DATE 2022-12-27 21:34:32 +01:00
the-djmaze
faa17ca3de Resolve #800 and #802 2022-12-27 20:57:11 +01:00
the-djmaze
0ff7b33a48 v2.24.2 2022-12-27 13:16:01 +01:00
the-djmaze
4ac3cba065 Disable sorting on thread view #445 2022-12-27 12:49:50 +01:00
the-djmaze
dfc76b4a47 Small fix for https://github.com/the-djmaze/snappymail/issues/445#issuecomment-1364575843 2022-12-27 12:22:27 +01:00
the-djmaze
5c9fdb6b55 Threaded view changes for https://github.com/the-djmaze/snappymail/issues/445#issuecomment-1364575843 2022-12-27 12:19:29 +01:00
the-djmaze
871664cd39 No need to call IMAP EXAMINE when current folder already SELECT 2022-12-27 11:17:29 +01:00
the-djmaze
ad45bcf2cf Changes for #796 2022-12-27 10:48:56 +01:00
the-djmaze
228fefc121 Resolve #798 2022-12-27 10:24:16 +01:00
the-djmaze
3f2d193bf6 Resolve #799 due to wrong implementation of RFC 8474 2022-12-27 10:11:54 +01:00
the-djmaze
c522daf866 No group per day for thread #445 2022-12-25 09:51:20 +01:00
the-djmaze
f2b8719223 Changes for #796 2022-12-24 23:21:43 +01:00
the-djmaze
ff9dfa28fe
Merge pull request #794 from mayswind/master
update Chinese translation
2022-12-24 19:22:05 +01:00
the-djmaze
432d3b5f47 Resolve #795 2022-12-24 19:21:32 +01:00
the-djmaze
c41049ab94 Improvements for #790 2022-12-24 19:03:41 +01:00
MaysWind
487f151dd0 update Chinese translation 2022-12-24 19:58:00 +08:00
the-djmaze
daa1185459 Resolve #793 2022-12-24 01:56:11 +01:00
the-djmaze
af59dd3d0d v2.24.1 2022-12-23 19:15:05 +01:00
the-djmaze
75b59322ad Resolve #792 2022-12-23 19:05:54 +01:00
the-djmaze
a83cb9150e Change Intl.DateTimeFormat() into toLocaleString() for iOS < 14 2022-12-23 19:03:35 +01:00
the-djmaze
508c262a4b SETTINGS_LABELS remove 'LABEL_' and '_NAME' part 2022-12-23 17:54:13 +01:00
the-djmaze
bf45070ff1 Remove unused Date.format() phpFormats 2022-12-23 16:28:02 +01:00
the-djmaze
27dddd3b1b Rollback #280 as it has issues 2022-12-23 16:20:54 +01:00
the-djmaze
9764c13fdd Remove unused receivedAt 2022-12-23 16:10:47 +01:00
the-djmaze
a9b336d90f 'Y-m-d\\TH:i:s' as 'ISO8601' 2022-12-23 16:10:35 +01:00
the-djmaze
a451600d8b Watch observable 2022-12-23 16:04:16 +01:00
the-djmaze
ea3eff5d0d Changes for #790 2022-12-23 13:50:31 +01:00
the-djmaze
79b19a978f Resolve #789 due to fix for #466 2022-12-23 13:48:20 +01:00
the-djmaze
16e513cf98 Avatars plugin fix for new MessageList group by day option 2022-12-23 12:27:46 +01:00
the-djmaze
b0ed74575f Cleanup locate time handling 2022-12-23 12:22:57 +01:00
the-djmaze
fca7e44aa0 Make MessageList per day optional #737 2022-12-23 08:42:36 +01:00
the-djmaze
81e46ccdf7 v2.24 screenshot 2022-12-22 20:11:34 +01:00
the-djmaze
a313664b64 Bugfix: PHP 7 issue with typed ?array 2022-12-22 20:03:55 +01:00
the-djmaze
e4b1950dbe v2.24.0 2022-12-22 19:40:11 +01:00
the-djmaze
18b7b39b49 Bugfix: https://github.com/the-djmaze/snappymail/issues/786#issuecomment-1363114868 2022-12-22 19:00:22 +01:00
the-djmaze
8aa9d3009b Resolve #737 by new messagelist view per day 2022-12-22 17:15:54 +01:00
the-djmaze
d8a64124f2 Resolve #466 2022-12-22 15:29:12 +01:00
the-djmaze
bb39d1709e Resolve #564 2022-12-22 14:54:41 +01:00
the-djmaze
6cd1007938 Split Domains and Extensions actions from ActionsAdmin in separate traits 2022-12-22 14:20:21 +01:00
the-djmaze
bafc5ad921 Move addComputables() from FolderModel.reviveFromJson to FolderModel.constructor 2022-12-22 12:00:02 +01:00
the-djmaze
c50670fc7f Rename printableUnreadCount() to unreadCount() 2022-12-22 11:40:56 +01:00
the-djmaze
cfdc37189b FolderList search style to CSS 2022-12-22 10:57:41 +01:00
the-djmaze
40d29acb15 Resolve #280 2022-12-22 10:31:19 +01:00
the-djmaze
7c4f1c309e Small cleanup 2022-12-22 10:03:02 +01:00
the-djmaze
9f69a992c8 Convert facebookmail.com to facebook.com 2022-12-22 10:02:47 +01:00
the-djmaze
215767ed9b Added: option to enable additional account unread messages count 2022-12-22 10:01:25 +01:00
the-djmaze
bad33e6dc4 Cleanup JavaScript MessageModel by moving dynamic code to MailMessageView and ComposePopupView 2022-12-22 09:56:06 +01:00
the-djmaze
ae1de36ee0 Bugfix: UnsubsribeLinks was broken 2022-12-22 00:17:13 +01:00
the-djmaze
277c567f54 Prevent godaddy click tracking 2022-12-21 20:53:48 +01:00
the-djmaze
315f2a345a Bugfix: handle multiple DKIM signatures authentication results 2022-12-21 20:51:33 +01:00
the-djmaze
219b155ede Cleanup Action reponse handling and use typed properties 2022-12-21 14:12:35 +01:00
the-djmaze
31b1096f4d Dark theme define color-scheme: dark; 2022-12-21 12:35:34 +01:00
the-djmaze
efd27cb211 CSS position data-unread better 2022-12-21 12:35:15 +01:00
the-djmaze
3012084184 Improved imapsync.php options detection 2022-12-21 12:34:54 +01:00
the-djmaze
2e3e1fa5fe Speedup Contacts Suggestions handling 2022-12-21 09:58:10 +01:00
the-djmaze
731ac52564 MailSo class properties to typed properties and clean up 2022-12-20 20:17:11 +01:00
the-djmaze
22450e7430 Cleanup MailSo 2022-12-19 17:26:59 +01:00
the-djmaze
a78fcb6308 partialParseResponseBranch broken due to previous commit 2022-12-19 10:44:25 +01:00
the-djmaze
8b65b6ee7c Bugfix: SieveClient quoted string parsing
Redesign NetClient::getNextBuffer() to return string
2022-12-19 10:39:50 +01:00
the-djmaze
2a86ab5753 Resolve https://github.com/RainLoop/rainloop-webmail/pull/2194 2022-12-18 23:59:12 +01:00
the-djmaze
5785ad511b Update domain default.json 2022-12-18 23:57:09 +01:00
the-djmaze
a21c859aba Cleanup Exception logging 2022-12-18 23:56:38 +01:00
Rene Hampölz
6529d01a4f fix: Admin setting with wrong alignment 2022-12-18 20:56:48 +00:00
Rene Hampölz
ef4ccdfb99 fix: Incorrectly colored separator 2022-12-18 20:52:18 +00:00
Rene Hampölz
c3c7aa9aef fix: Admin setting with wrong alignment 2022-12-18 20:44:40 +00:00
Rene Hampölz
fed0d53425 chore: Add alerts 2022-12-18 20:33:29 +00:00
Rene Hampölz
aa1128d40c refactor: Improve structure 2022-12-18 19:56:22 +00:00
Rene Hampölz
f88643ce65 chore: Improve theme selector 2022-12-18 19:50:01 +00:00
Rene Hampölz
980f1e45ef chore: Improve some buttons 2022-12-18 19:49:10 +00:00
Rene Hampölz
2ea3f6de4a fix: Checkbox overlaps with other elements 2022-12-18 19:35:53 +00:00
Rene Hampölz
c5f419e8da chore: Add theme selector 2022-12-18 19:26:52 +00:00
Rene Hampölz
34d8cf002b fix: Some buttons look slightly different 2022-12-18 19:01:46 +00:00
Rene Hampölz
9306808263 chore: Hide unnecessary theme settings 2022-12-18 16:48:57 +00:00
Rene Hampölz
9c7f522c82 chore: Improve admin settings pane margin 2022-12-18 16:31:51 +00:00
Rene Hampölz
d7af7d15a0 chore: Improve settings pane margin 2022-12-18 16:30:16 +00:00
Rene Hampölz
ef69e31ad1 chore: Improve settings menu buttons 2022-12-18 16:11:37 +00:00
the-djmaze
d126380282 Count() to count() 2022-12-18 13:55:37 +01:00
the-djmaze
f5beeb9ca1 Check SMTP SIZE #779 2022-12-18 13:55:22 +01:00
the-djmaze
2793dae547 IsSupported() to hasCapability() 2022-12-18 13:55:00 +01:00
Rene Hampölz
cdb4c0a3cf chore: Add admin side panel 2022-12-17 21:48:57 +00:00
Rene Hampölz
4aa684e186 chore: Add tables 2022-12-17 21:27:54 +00:00
Rene Hampölz
ed01a4df61 chore: Improve control group alignment 2022-12-17 20:19:39 +00:00
Rene Hampölz
9a3c5c17b7 fix: Wrong height of entries in settings menu 2022-12-17 20:00:34 +00:00
Rene Hampölz
757dc78bad fix: Broken checkboxes in the latest snappymail release 2022-12-17 19:43:47 +00:00
Rene Hampölz
88ad0ccc6c chore: Improve unread count badge 2022-12-17 19:16:55 +00:00
the-djmaze
19bc530879 Rename ImapClient::IsSupported() to ImapClient::hasCapability() 2022-12-16 11:52:56 +01:00
the-djmaze
3fdb2e7698 Better fix for #780 2022-12-16 11:45:24 +01:00
the-djmaze
edd083801f Resolve #780 2022-12-16 11:41:51 +01:00
the-djmaze
9484ff3cbb Resolve #769 2022-12-16 09:45:18 +01:00
the-djmaze
26706acf6a Added more imapsync CLI options and help 2022-12-16 09:32:10 +01:00
Rene Hampölz
ae4a71f9a5 chore: Improve padding of message list footer 2022-11-30 21:24:07 +00:00
Rene Hampölz
4823036995 fix: Imperfectly aligned icons 2022-11-30 21:18:27 +00:00
Rene Hampölz
a57ead9f7c chore: Improve buttons of message list toolbar 2022-11-30 20:58:25 +00:00
Rene Hampölz
06300de9f8 Fix: Broken styles in latest snappymail release 2022-11-30 20:50:53 +00:00
Rene Hampölz
095bd04634 fix: Sidebar button text is visible when collapsed 2022-11-19 15:20:53 +00:00
Rene Hampölz
8b508456b2 fix: Broken styles in embed mode 2022-11-19 15:17:26 +00:00
Rene Hampölz
d37022b116 chore: Rework checkboxes for better browser compatibility 2022-11-19 14:08:29 +00:00
Rene Hampölz
6281b5eee2 fix: Broken checkboxes in embed mode 2022-11-19 12:20:18 +00:00
Rene Hampölz
65e614f00f chore: Add checkboxes 2022-11-13 11:26:15 +00:00
Rene Hampölz
23b31c7c26 fix: Broken styles in embed mode 2022-11-12 16:23:07 +00:00
Rene Hampölz
5f19bb8699 fix: Passed stylesheets are 'unloaded' after some time 2022-11-12 16:15:21 +00:00
Rene Hampölz
2d6e2deb3d revert: Nextcloud v25 styles in upstream style file 2022-11-07 16:35:26 +00:00
Rene Hampölz
8d2446f314 chore: Move files to new theme folder 2022-11-07 16:24:08 +00:00
Rene Hampölz
7da114b0c9 fix: nextcloud theme font-size is not used 2022-11-05 16:52:45 +00:00
Rene Hampölz
94506ed726 fix: nextcloud theme font-family is not used 2022-11-05 16:45:29 +00:00
Rene Hampölz
2f8203abda fix formatting 2022-11-05 15:27:24 +00:00
Rene Hampölz
ad8da7dce0 Initial commit 2022-11-05 10:53:07 +00:00
1867 changed files with 137920 additions and 50568 deletions

View file

@ -1,51 +1,192 @@
# syntax=docker/dockerfile:1
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
COPY package.json yarn.lock ./
RUN yarn install
COPY . .
# Patch release.php with hotfix from: https://github.com/xgbstar1/snappymail-docker/blob/main/Dockerfile, so that release.php doesn't fail with error
RUN sed -i 's_^if.*rename.*snappymail.v.0.0.0.*$_if (!!system("mv snappymail/v/0.0.0 snappymail/v/{$package->version}")) {_' cli/release.php || true
RUN php release.php
RUN set -eux; \
VERSION=$( ls build/dist/releases/webmail ); \
ls -al build/dist/releases/webmail/$VERSION/snappymail-$VERSION.tar.gz; \
mkdir -p /snappymail; \
tar -zxvf build/dist/releases/webmail/$VERSION/snappymail-$VERSION.tar.gz -C /snappymail; \
find /snappymail -type d -exec chmod 550 {} \; ; \
find /snappymail -type f -exec chmod 440 {} \; ; \
find /snappymail/data -type d -exec chmod 750 {} \; ; \
# Remove unneeded files
rm -v /snappymail/README.md /snappymail/_include.php
# Inspired by the original Rainloop dockerfile from youtous on GitLab
FROM php:8.1-fpm-bullseye
FROM php:8.5-fpm-alpine AS final
ARG FILES_ZIP
LABEL org.label-schema.description="SnappyMail webmail client image using nginx, php-fpm based on Debian Buster"
ENV UID=991 GID=991 UPLOAD_MAX_SIZE=25M LOG_TO_STDERR=true MEMORY_LIMIT=128M SECURE_COOKIES=true
ENV fpm.pool.clear_env=false
LABEL org.label-schema.description="SnappyMail webmail client image using nginx, php-fpm on Alpine"
# Install dependencies such as nginx
RUN mkdir -p /usr/share/man/man1/ /usr/share/man/man3/ /usr/share/man/man7/ && \
apt-get update -q --fix-missing && \
apt-get -y upgrade && \
apt-get install --no-install-recommends -y \
apt-transport-https gnupg openssl wget curl ca-certificates nginx supervisor sudo \
unzip libzip-dev libxml2-dev libldb-dev libldap2-dev \
sqlite3 libsqlite3-dev libsqlite3-0 libpq-dev postgresql-client mariadb-client logrotate \
zip mlocate libpcre3-dev libicu-dev \
build-essential chrpath libssl-dev \
libxft-dev libfreetype6 libfreetype6-dev \
libpng-dev libjpeg62-turbo-dev \
libfontconfig1 libfontconfig1-dev \
&& \
rm -rf /var/lib/apt/lists/*
RUN apk add --no-cache ca-certificates nginx supervisor bash
# Install PHP extensions
RUN php -m && \
docker-php-ext-configure ldap --with-libdir=lib/$(uname -m)-linux-gnu/ && \
docker-php-ext-configure intl && \
docker-php-ext-configure gd --with-freetype --with-jpeg && \
docker-php-ext-install ldap opcache pdo_mysql pdo_pgsql zip intl gd && \
php -m
# apcu
RUN set -eux; \
apk add --no-cache --virtual .build-dependencies $PHPIZE_DEPS; \
pecl install apcu; \
docker-php-ext-enable apcu; \
docker-php-source delete; \
apk del .build-dependencies;
# gd
RUN set -eux; \
apk add --no-cache freetype libjpeg-turbo libpng; \
apk add --no-cache --virtual .deps freetype-dev libjpeg-turbo-dev libpng-dev; \
docker-php-ext-configure gd --with-freetype --with-jpeg; \
docker-php-ext-install gd; \
apk del .deps
# gmagick
# RUN set -eux; \
# apk add --no-cache graphicsmagick libgomp; \
# apk add --no-cache --virtual .deps graphicsmagick-dev libtool; \
# apk add --no-cache --virtual .build-dependencies $PHPIZE_DEPS; \
# pecl install gmagick-2.0.6RC1; \
# docker-php-ext-enable gmagick; \
# docker-php-source delete; \
# apk del .build-dependencies; \
# apk del .deps
# gnupg
RUN set -eux; \
apk add --no-cache gnupg gpgme; \
apk add --no-cache --virtual .deps gpgme-dev; \
apk add --no-cache --virtual .build-dependencies $PHPIZE_DEPS; \
pecl install gnupg; \
docker-php-ext-enable gnupg; \
docker-php-source delete; \
apk del .build-dependencies; \
apk del .deps
# imagick
RUN set -eux; \
apk add --no-cache imagemagick libgomp; \
apk add --no-cache --virtual .deps imagemagick-dev; \
apk add --no-cache --virtual .build-dependencies $PHPIZE_DEPS; \
echo | pecl install imagick; \
docker-php-ext-enable imagick; \
docker-php-source delete; \
apk del .build-dependencies; \
apk del .deps
# intl
RUN set -eux; \
apk add --no-cache icu-libs; \
apk add --no-cache --virtual .deps icu-dev; \
docker-php-ext-configure intl; \
docker-php-ext-install intl; \
apk del .deps
# ldap
RUN set -eux; \
apk add --no-cache libldap; \
apk add --no-cache --virtual .deps openldap-dev; \
docker-php-ext-configure ldap; \
docker-php-ext-install ldap; \
apk del .deps
# mysql
RUN docker-php-ext-install pdo_mysql
# postgres
RUN set -eux; \
apk add --no-cache postgresql-libs; \
apk add --no-cache --virtual .deps postgresql-dev; \
docker-php-ext-install pdo_pgsql; \
apk del .deps
# redis
RUN set -eux; \
apk add --no-cache liblzf zstd-libs; \
apk add --no-cache --virtual .deps zstd-dev; \
apk add --no-cache --virtual .build-dependencies $PHPIZE_DEPS; \
pecl install igbinary; \
docker-php-ext-enable igbinary; \
pecl install --configureoptions 'enable-redis-igbinary="yes" enable-redis-lzf="yes" enable-redis-zstd="yes"' redis; \
docker-php-ext-enable redis; \
docker-php-source delete; \
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; \
apk add --no-cache --virtual .deps tidyhtml-dev; \
docker-php-ext-install tidy; \
apk del .deps
# uuid
RUN set -eux; \
apk add --no-cache libuuid; \
apk add --no-cache --virtual .deps util-linux-dev; \
apk add --no-cache --virtual .build-dependencies $PHPIZE_DEPS; \
pecl install uuid; \
docker-php-ext-enable uuid; \
docker-php-source delete; \
apk del .build-dependencies; \
apk del .deps
# xxtea - Manually install php8 compatible version from https://github.com/xxtea/xxtea-pecl master branch
RUN set -eux; \
apk add --no-cache --virtual .build-dependencies $PHPIZE_DEPS; \
wget -q https://github.com/xxtea/xxtea-pecl/tarball/3f5888a29045e12301254151737c5dab4523a1c1 -O xxtea.tar; \
echo '9cbfd9c27255767deb26ddedf69e738d401d88ac9762d82c8510f9768842ca18 xxtea.tar' | sha256sum -c -; \
tar -C /usr/src -xvf xxtea.tar; \
cd /usr/src/xxtea-xxtea-pecl-3f5888a; \
phpize; \
./configure --with-php-config=/usr/local/bin/php-config --enable-xxtea=yes; \
make install; \
docker-php-ext-enable xxtea; \
cd -; \
rm -fv xxtea.tar; \
rm -rfv /usr/src/xxtea*; \
apk del .build-dependencies;
# zip
RUN set -eux; \
apk add --no-cache libzip; \
apk add --no-cache --virtual .deps libzip-dev; \
docker-php-ext-install zip; \
apk del .deps
# Install snappymail
WORKDIR /tmp
COPY ${FILES_ZIP} .
RUN mkdir /snappymail && \
unzip -q ${FILES_ZIP} -d /snappymail && \
find /snappymail -type d -exec chmod 755 {} \; && \
find /snappymail -type f -exec chmod 644 {} \; && \
rm -rf ${FILES_ZIP}
# The 'www-data' user/group in alpine is 82:82. The 'nginx' user/group in alpine is 101:101, and is part of www-data group
COPY --chown=www-data:www-data --from=builder /snappymail /snappymail
# Use a custom snappymail data folder
RUN mv -v /snappymail/data /var/lib/snappymail;
# Setup configs
COPY --chown=root:root .docker/release/files /
RUN set -eux; \
chown www-data:www-data /snappymail/include.php; \
chmod 440 /snappymail/include.php; \
chmod +x /entrypoint.sh; \
# Disable the built-in php-fpm configs, since we're using our own config
mv -v /usr/local/etc/php-fpm.d/docker.conf /usr/local/etc/php-fpm.d/docker.conf.disabled; \
mv -v /usr/local/etc/php-fpm.d/www.conf /usr/local/etc/php-fpm.d/www.conf.disabled; \
mv -v /usr/local/etc/php-fpm.d/zz-docker.conf /usr/local/etc/php-fpm.d/zz-docker.conf.disabled;
# Install other content
COPY files /
RUN chmod +x /entrypoint.sh && chmod +x /logrotate-loop.sh
VOLUME /snappymail/data
USER root
WORKDIR /snappymail
VOLUME /var/lib/snappymail
EXPOSE 8888
EXPOSE 9000
ENTRYPOINT []
CMD ["/entrypoint.sh"]

96
.docker/release/files/entrypoint.sh Normal file → Executable file
View file

@ -1,23 +1,26 @@
#!/bin/sh
set -eu
# Create not root user
groupadd --gid "$GID" php-cli -f
adduser --uid "$UID" --disabled-password --gid "$GID" --shell /bin/bash --home /home/php-cli php-cli --force --gecos ""
DEBUG=${DEBUG:-}
if [ "$DEBUG" = 'true' ]; then
set -x
fi
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
# Set log output to STDERR if wanted (LOG_TO_STDERR=true)
if [ "$LOG_TO_STDERR" = true ]; then
echo "[INFO] Logging to stderr activated"
sed -i "s/.*error_log.*$/error_log \/dev\/stderr warn;/" /etc/nginx/nginx.conf
sed -i "s/.*error_log.*$/php_admin_value[error_log] = \/dev\/stderr/" /usr/local/etc/php-fpm.d/php-fpm.conf
fi
# Secure cookies
if [ "${SECURE_COOKIES}" = true ]; then
if [ "${SECURE_COOKIES}" = 'true' ]; then
echo "[INFO] Secure cookies activated"
{
echo 'session.cookie_httponly = On';
@ -26,43 +29,58 @@ if [ "${SECURE_COOKIES}" = true ]; then
} > /usr/local/etc/php/conf.d/cookies.ini;
fi
# Copy snappymail default config if absent
SNAPPYMAIL_CONFIG_FILE=/snappymail/data/_data_/_default_/configs/application.ini
echo "[INFO] Snappymail version: $( ls /snappymail/snappymail/v )"
# Set permissions on snappymail data
echo "[INFO] Setting permissions on /var/lib/snappymail"
chown -R www-data:www-data /var/lib/snappymail/
chmod 550 /var/lib/snappymail/
find /var/lib/snappymail/ -type d -exec chmod 750 {} \;
# Create snappymail default config if absent
SNAPPYMAIL_CONFIG_FILE=/var/lib/snappymail/_data_/_default_/configs/application.ini
if [ ! -f "$SNAPPYMAIL_CONFIG_FILE" ]; then
echo "[INFO] Creating default Snappymail configuration"
mkdir -p $(dirname $SNAPPYMAIL_CONFIG_FILE)
cp /usr/local/include/application.ini $SNAPPYMAIL_CONFIG_FILE
echo "[INFO] Creating default Snappymail configuration: $SNAPPYMAIL_CONFIG_FILE"
# Run snappymail and exit. This populates the snappymail data directory and generates the config file
# On error, print php exception and exit
EXITCODE=
su - www-data -s /bin/sh -c 'php /snappymail/index.php' > /tmp/out || EXITCODE=$?
if [ -n "$EXITCODE" ]; then
cat /tmp/out
exit "$EXITCODE"
fi
fi
echo "[INFO] Overriding values in snappymail configuration: $SNAPPYMAIL_CONFIG_FILE"
# Enable output of snappymail logs
if [ "${LOG_TO_STDERR}" = true ]; then
sed -z 's/\; Enable logging\nenable = Off/\; Enable logging\nenable = On/' -i $SNAPPYMAIL_CONFIG_FILE
sed 's/^filename = .*/filename = "errors.log"/' -i $SNAPPYMAIL_CONFIG_FILE
sed 's/^write_on_error_only = .*/write_on_error_only = Off/' -i $SNAPPYMAIL_CONFIG_FILE
sed 's/^write_on_php_error_only = .*/write_on_php_error_only = On/' -i $SNAPPYMAIL_CONFIG_FILE
else
sed -z 's/\; Enable logging\nenable = On/\; Enable logging\nenable = Off/' -i $SNAPPYMAIL_CONFIG_FILE
fi
sed '/^\; Enable logging/{
N
s/enable = Off/enable = On/
}' -i $SNAPPYMAIL_CONFIG_FILE
# Redirect snappymail logs to stderr /stdout
sed 's/^filename = .*/filename = "stderr"/' -i $SNAPPYMAIL_CONFIG_FILE
sed 's/^write_on_error_only = .*/write_on_error_only = Off/' -i $SNAPPYMAIL_CONFIG_FILE
sed 's/^write_on_php_error_only = .*/write_on_php_error_only = On/' -i $SNAPPYMAIL_CONFIG_FILE
# Always enable snappymail Auth logging
sed 's/^auth_logging = .*/auth_logging = On/' -i $SNAPPYMAIL_CONFIG_FILE
sed 's/^auth_logging_filename = .*/auth_logging_filename = "auth.log"/' -i $SNAPPYMAIL_CONFIG_FILE
sed 's/^auth_logging_format = .*/auth_logging_format = "[{date:Y-m-d H:i:s}] Auth failed: ip={request:ip} user={imap:login} host={imap:host} port={imap:port}"/' -i $SNAPPYMAIL_CONFIG_FILE
# Redirect snappymail logs to stderr /stdout
mkdir -p /snappymail/data/_data_/_default_/logs/
# empty logs
cp /dev/null /snappymail/data/_data_/_default_/logs/errors.log
cp /dev/null /snappymail/data/_data_/_default_/logs/auth.log
chown -R php-cli:php-cli /snappymail/data/
sed 's/^auth_syslog = .*/auth_syslog = Off/' -i $SNAPPYMAIL_CONFIG_FILE
# Fix permissions
chown -R $UID:$GID /snappymail/data /var/log /var/lib/nginx
chmod o+w /dev/stdout
chmod o+w /dev/stderr
(
while ! nc -vz -w 1 127.0.0.1 8888 > /dev/null 2>&1; do echo "[INFO] Checking whether nginx is alive"; sleep 1; done
while ! nc -vz -w 1 127.0.0.1 9000 > /dev/null 2>&1; do echo "[INFO] Checking whether php-fpm is alive"; sleep 1; done
# Create snappymail admin password if absent
SNAPPYMAIL_ADMIN_PASSWORD_FILE=/var/lib/snappymail/_data_/_default_/admin_password.txt
if [ ! -f "$SNAPPYMAIL_ADMIN_PASSWORD_FILE" ]; then
echo "[INFO] Creating Snappymail admin password file: $SNAPPYMAIL_ADMIN_PASSWORD_FILE"
wget -T 1 -qO- 'http://127.0.0.1:8888/?/AdminAppData/0/12345/' > /dev/null
echo "[INFO] Snappymail Admin Panel ready at http://localhost:8888/?admin. Login using password in $SNAPPYMAIL_ADMIN_PASSWORD_FILE"
fi
# Touch supervisord PID file in order to fix permissions
touch /run/supervisord.pid
chown php-cli:php-cli /run/supervisord.pid
wget -T 1 -qO- 'http://127.0.0.1:8888/' > /dev/null
echo "[INFO] Snappymail ready at http://localhost:8888/"
) &
# RUN !
exec sudo -u php-cli -g php-cli /usr/bin/supervisord -c '/supervisor.conf' --pidfile '/run/supervisord.pid'
exec /usr/bin/supervisord -c /supervisor.conf --pidfile /run/supervisord.pid

View file

@ -1,5 +0,0 @@
/snappymail/data/_data_/_default_/logs/* {
size 10M
rotate 0
missingok
}

View file

@ -11,7 +11,7 @@ http {
default_type application/octet-stream;
access_log off;
error_log /tmp/ngx_error.log error;
error_log /dev/stderr error;
sendfile on;
keepalive_timeout 15;
@ -52,6 +52,8 @@ http {
server {
listen 8888;
listen [::]:8888;
#listen [::]:8888 ipv6only=off;
root /snappymail;
index index.php;
charset utf-8;
@ -95,7 +97,7 @@ http {
fastcgi_param PATH_INFO $fastcgi_path_info;
fastcgi_param HTTP_PROXY "";
fastcgi_index index.php;
fastcgi_pass unix:/tmp/php-fpm.sock;
fastcgi_pass 127.0.0.1:9000;
fastcgi_intercept_errors on;
fastcgi_request_buffering off;
fastcgi_param REMOTE_ADDR $http_x_real_ip;

View file

@ -1,113 +0,0 @@
#!/bin/bash
# ----------------------------------------------------------------------
# Simple script to invoke logrotate at regular intervals
# ----------------------------------------------------------------------
# from https://github.com/misho-kr/docker-appliances/blob/master/nginx-nodejs/logrotate-loop.sh
LOGROTATE_BIN="logrotate"
STATE="$HOME/logrotate.state"
CONF="/etc/logrotate.d/snappymail"
export LOGROTATE_BIN STATE CONF
RUN_INTERVAL="3600" # every hour
# helper functions for logging
export FMT="%a %b %d %Y %H:%M:%S GMT%z (%Z)"
function log_date() {
echo "$(date +"$FMT"): $*"
}
# ----------------------------------------------------------------------
# Main loop of the logrotate service:
#
# while True:
# sleep N seconds
# run logrotate
#
# ----------------------------------------------------------------------
function logrotate_loop() {
trap on_terminate TERM INT
local interval="${1}"
log_date "===================================================="
log_date
log_date "logrotate service starting (pid=$$)"
log_date "logrotate process will run every ${interval} seconds"
while true; do
current_time=$(date "+%s")
next_run_time=$(( current_time + interval ))
while (( current_time < next_run_time ))
do
logrotate_sleep $(( next_run_time - current_time ))
current_time=$(date "+%s")
done
logrotate_run
done
}
# helper function to execute logrotate and pass it the right parameters
function logrotate_run() {
log_date "logrotate will run now"
${LOGROTATE_BIN} -s ${STATE} ${CONF}
}
# ----------------------------------------------------------------------
# Procedure to idle the execution for a number of seconds
#
# There are two requirements:
#
# - export the PID of the sleep command so that it can be terminated
# in case the logrotate service is being shutdown
# - keep this (bash) process responsive to SIGTERM while in sleep
# mode (normally the signal will be masked and will not be delivered
# until the subprocess completes)
# ----------------------------------------------------------------------
proc_sleep_pid=""
function logrotate_sleep() {
local sleep_interval=${1}
log_date "logrotate will sleep for ${sleep_interval} seconds"
( exec -a "logrotate: sleep" sleep ${sleep_interval} )&
proc_sleep_pid=$!
wait ${proc_sleep_pid}
}
# ----------------------------------------------------------------------
# Signal handler for logrotate service to make sure the process exits:
#
# - properly by terminating the sleep process that is used to idle
# the service
# - gracefully by writing a message in the log
# ----------------------------------------------------------------------
function on_terminate() {
log_date "logrotate will terminate"
log_date
kill -TERM ${proc_sleep_pid}
exit 0
}
# ----------------------------------------------------------------------
# main
# ----------------------------------------------------------------------
logrotate_loop ${1:-RUN_INTERVAL}

View file

@ -0,0 +1,3 @@
<?php
define('APP_DATA_FOLDER_PATH', '/var/lib/snappymail/');
?>

View file

@ -1,10 +1,13 @@
[supervisord]
nodaemon=true
user=root
logfile=/dev/null
logfile_maxbytes=0
[program:nginx]
command=nginx -c /etc/nginx/nginx.conf -g 'daemon off;'
process_name=%(program_name)s_%(process_num)02d
user=php-cli
user=root
numprocs=1
autostart=true
autorestart=false
@ -17,7 +20,7 @@ stderr_logfile_maxbytes=0
[program:php-fpm]
command=php-fpm -F
process_name=%(program_name)s_%(process_num)02d
user=php-cli
user=root
numprocs=1
autostart=true
autorestart=false
@ -27,34 +30,11 @@ stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
; reads snappymail logs
[program:snappymail-auth]
command=tail -f /snappymail/data/_data_/_default_/logs/auth.log
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
[program:snappymail-errors]
command=tail -f /snappymail/data/_data_/_default_/logs/errors.log
# everything is an error
stdout_logfile=/dev/stderr
stdout_logfile_maxbytes=0
redirect_stderr=true
[program:logrotate]
command=/logrotate-loop.sh
autorestart=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
[eventlistener:subprocess-stopped]
command=php /listener.php
process_name=%(program_name)s_%(process_num)02d
user=php-cli
user=root
numprocs=1
events=PROCESS_STATE_EXITED,PROCESS_STATE_STOPPED,PROCESS_STATE_FATAL
autostart=true
autorestart=unexpected
autorestart=unexpected

View file

@ -1,15 +1,21 @@
[global]
daemonize = no
error_log = /dev/stderr
log_buffering = no
[default]
listen = /tmp/php-fpm.sock
listen = 9000
user = www-data
listen.owner = www-data
listen.group = www-data
pm = ondemand
pm.max_children = 30
pm.process_idle_timeout = 10s
pm.max_requests = 500
catch_workers_output = yes
decorate_workers_output = no
chdir = /
php_admin_value[error_log] = /tmp/php_error.log
pm.status_path = /status
php_admin_value[log_errors] = On
php_admin_value[expose_php] = Off
php_admin_value[display_errors] = Off

View file

@ -1,293 +0,0 @@
; SnappyMail configuration file
; Please don't add custom parameters here, those will be overwritten
[webmail]
; Text displayed as page title
title = "SnappyMail Webmail"
; Text displayed on startup
loading_description = "SnappyMail"
favicon_url = ""
app_path = ""
; Theme used by default
theme = "Default"
; Allow theme selection on settings screen
allow_themes = On
allow_user_background = Off
; Language used by default
language = "en"
; Admin Panel interface language
language_admin = "en"
; Allow language selection on settings screen
allow_languages_on_settings = On
allow_additional_accounts = On
allow_additional_identities = On
; Number of messages displayed on page by default
messages_per_page = 20
; Mark message read after N seconds
message_read_delay = 5
; File size limit (MB) for file upload on compose screen
; 0 for unlimited.
attachment_size_limit = 2
[interface]
show_attachment_thumbnail = On
[contacts]
; Enable contacts
enable = Off
allow_sync = Off
sync_interval = 20
type = "sqlite"
pdo_dsn = "host=127.0.0.1;port=3306;dbname=snappymail"
pdo_user = "root"
pdo_password = ""
suggestions_limit = 30
[security]
; Enable CSRF protection (http://en.wikipedia.org/wiki/Cross-site_request_forgery)
csrf_protection = On
custom_server_signature = "SnappyMail"
x_xss_protection_header = "1; mode=block"
openpgp = Off
; Login and password for web admin panel
admin_login = "admin"
admin_password = ""
admin_totp = ""
; Access settings
allow_admin_panel = On
hide_x_mailer_header = On
admin_panel_host = ""
admin_panel_key = "admin"
content_security_policy = ""
csp_report = Off
encrypt_cipher = "aes-256-cbc-hmac-sha1"
[admin_panel]
allow_update = Off
[ssl]
; Require verification of SSL certificate used.
verify_certificate = Off
; Allow self-signed certificates. Requires verify_certificate.
allow_self_signed = On
; https://www.openssl.org/docs/man1.1.1/man3/SSL_CTX_set_security_level.html
security_level = 1
; Location of Certificate Authority file on local filesystem (/etc/ssl/certs/ca-certificates.crt)
cafile = ""
; capath must be a correctly hashed certificate directory. (/etc/ssl/certs/)
capath = ""
; Location of client certificate file (pem format with private key) on local filesystem
client_cert = ""
[capa]
quota = On
dangerous_actions = On
message_actions = On
attachments_actions = On
[login]
default_domain = ""
; Allow language selection on webmail login screen
allow_languages_on_login = On
determine_user_language = On
determine_user_domain = Off
login_lowercase = On
; This option allows webmail to remember the logged in user
; once they closed the browser window.
;
; Values:
; "DefaultOff" - can be used, disabled by default;
; "DefaultOn" - can be used, enabled by default;
; "Unused" - cannot be used
sign_me_auto = "DefaultOff"
[plugins]
; Enable plugin support
enable = Off
; List of enabled plugins
enabled_list = ""
[defaults]
; Editor mode used by default (Plain, Html)
view_editor_type = "Html"
; layout: 0 - no preview, 1 - side preview, 2 - bottom preview
view_layout = 1
view_use_checkboxes = On
autologout = 30
view_html = On
show_images = Off
contacts_autosave = On
mail_use_threads = Off
allow_draft_autosave = On
mail_reply_same_folder = Off
[logs]
; Enable logging
enable = Off
; Log messages of set RFC 5424 section 6.2.1 Severity level and higher (0 = highest, 7 = lowest).
; 0 = Emergency
; 1 = Alert
; 2 = Critical
; 3 = Error
; 4 = Warning
; 5 = Notice
; 6 = Informational
; 7 = Debug
level = 4
; Required for development purposes only.
; Disabling this option is not recommended.
hide_passwords = On
time_zone = "UTC"
; Log filename.
; For security reasons, some characters are removed from filename.
; Allows for pattern-based folder creation (see examples below).
;
; Patterns:
; {date:Y-m-d} - Replaced by pattern-based date
; Detailed info: http://www.php.net/manual/en/function.date.php
; {user:email} - Replaced by user's email address
; If user is not logged in, value is set to "unknown"
; {user:login} - Replaced by user's login (the user part of an email)
; If user is not logged in, value is set to "unknown"
; {user:domain} - Replaced by user's domain name (the domain part of an email)
; If user is not logged in, value is set to "unknown"
; {user:uid} - Replaced by user's UID regardless of account currently used
;
; {user:ip}
; {request:ip} - Replaced by user's IP address
;
; Others:
; {imap:login} {imap:host} {imap:port}
; {smtp:login} {smtp:host} {smtp:port}
;
; Examples:
; filename = "log-{date:Y-m-d}.txt"
; filename = "{date:Y-m-d}/{user:domain}/{user:email}_{user:uid}.log"
; filename = "{user:email}-{date:Y-m-d}.txt"
; filename = "syslog"
filename = "log-{date:Y-m-d}.txt"
; Enable auth logging in a separate file (for fail2ban)
auth_logging = Off
auth_logging_filename = "fail2ban/auth-{date:Y-m-d}.txt"
auth_logging_format = "[{date:Y-m-d H:i:s}] Auth failed: ip={request:ip} user={imap:login} host={imap:host} port={imap:port}"
; Enable auth logging to syslog for fail2ban
auth_syslog = On
[debug]
; Special option required for development purposes
enable = Off
[cache]
; The section controls caching of the entire application.
;
; Enables caching in the system
enable = On
; Additional caching key. If changed, cache is purged
index = "v1"
; Can be: files, APCU, memcache, redis (beta)
fast_cache_driver = "files"
; Additional caching key. If changed, fast cache is purged
fast_cache_index = "v1"
; Browser-level cache. If enabled, caching is maintainted without using files
http = On
; Browser-level cache time (seconds, Expires header)
http_expires = 3600
; Caching message UIDs when searching and sorting (threading)
server_uids = On
[imap]
use_sort = On
use_force_selection = Off
use_thread = On
use_move = Off
use_expunge_all_on_delete = Off
body_text_limit = 555000
message_list_fast_simple_search = On
message_list_count_limit_trigger = 0
message_list_date_filter = 0
message_list_permanent_filter = ""
message_all_headers = Off
large_thread_limit = 50
folder_list_limit = 200
show_login_alert = On
use_list_status = On
timeout = 300
disable_metadata = Off
[labs]
cache_system_data = On
date_from_headers = On
autocreate_system_folders = Off
allow_message_append = Off
login_fault_delay = 1
log_ajax_response_write_limit = 300
allow_html_editor_biti_buttons = Off
allow_ctrl_enter_on_compose = On
try_to_detect_hidden_images = Off
use_app_debug_js = Off
use_app_debug_css = Off
smtp_show_server_errors = Off
smtp_timeout = 60
sieve_auth_plain_initial = On
sieve_allow_fileinto_inbox = Off
sieve_timeout = 10
sasl_allow_plain = On
sasl_allow_scram_sha = Off
sasl_allow_cram_md5 = Off
mail_func_clear_headers = On
mail_func_additional_parameters = Off
folders_spec_limit = 50
curl_proxy = ""
curl_proxy_auth = ""
force_https = Off
custom_login_link = ""
custom_logout_link = ""
http_client_ip_check_proxy = Off
fast_cache_memcache_host = "127.0.0.1"
fast_cache_memcache_port = 11211
fast_cache_redis_host = "127.0.0.1"
fast_cache_redis_port = 6379
use_local_proxy_for_external_images = On
image_exif_auto_rotate = Off
cookie_default_path = ""
cookie_default_secure = Off
check_new_messages = On
replace_env_in_configuration = ""
boundary_prefix = ""
dev_email = ""
dev_password = ""
[version]
current = "2.23.1"
saved = "Fri, 30 Sep 2022 12:34:56 +0000"

View file

@ -0,0 +1,7 @@
#!/bin/sh
# This script uses docker buildx to build a docker image, and loads it into the docker daemon. Then it executes ./test.sh to test the docker image
# It is useful for testing release builds in development
set -eu
IMAGE=snappymail/snappymail:test
DOCKER_BUILDX=1 docker build -t "$IMAGE" -f .docker/release/Dockerfile .
.docker/release/test/test.sh "$IMAGE"

View file

@ -0,0 +1,29 @@
# See: https://github.com/GoogleContainerTools/container-structure-test
# See: https://github.com/GoogleContainerTools/container-structure-test/issues/78
schemaVersion: 2.0.0
commandTests:
- name: Integration test
command: /bin/sh
args:
- -c
- |
set -eux
DEBUG=true /entrypoint.sh > /tmp/test 2>&1 &
sleep 5
cat /tmp/test
pidof supervisord
pidof nginx
pidof php-fpm
ls -al /var/lib/snappymail/_data_/_default_/configs/application.ini
ls -al /var/lib/snappymail/_data_/_default_/admin_password.txt
nc -vz 127.0.0.1 8888
nc -vz 127.0.0.1 9000
wget -S -T 3 -O /dev/null http://127.0.0.1:8888
kill `pidof supervisord`
metadataTest:
exposedPorts: ["8888", "9000"]
volumes: ["/var/lib/snappymail"]
entrypoint: []
cmd: ["/entrypoint.sh"]
workdir: /snappymail
user: root

10
.docker/release/test/test.sh Executable file
View file

@ -0,0 +1,10 @@
#!/bin/sh
# This script tests a given docker image using https://github.com/GoogleContainerTools/container-structure-test
set -eu
SCRIPT_DIR=$( cd "$(dirname "$0")" && pwd )
IMAGE=${1:-}
echo "Testing image: $IMAGE"
docker run --rm -i \
-v /var/run/docker.sock:/var/run/docker.sock:ro \
-v "$SCRIPT_DIR/config.yaml:/config.yaml:ro" \
gcr.io/gcp-runtimes/container-structure-test:latest test --image "$IMAGE" --config config.yaml

2
.dockerignore Normal file
View file

@ -0,0 +1,2 @@
/.git
/node_modules

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: {
@ -35,10 +35,17 @@ module.exports = {
// vendors/bootstrap/bootstrap.native.js
'BSN': "readonly",
// Mailvelope
'mailvelope': "readonly"
'mailvelope': "readonly",
// Punycode
'IDN': "readonly",
// Turndown
'TurndownService': "readonly",
// Marked
'marked': "readonly"
},
// http://eslint.org/docs/rules/
rules: {
'no-cond-assign': 0,
// plugins
'no-mixed-spaces-and-tabs': 'off',
'max-len': [

2
.github/FUNDING.yml vendored
View file

@ -1,2 +1,2 @@
community_bridge: SnappyMail
github: the-djmaze
custom: ["https://www.paypal.me/thedjmaze", "https://snappymail.eu"]

View file

@ -28,9 +28,11 @@ If applicable, add screenshots to help explain your problem.
- IMAP daemon: [e.g. courier, dovecot]
- PHP version:
- 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.

93
.github/workflows/docker-pr.yml vendored Normal file
View file

@ -0,0 +1,93 @@
name: docker-pr
on:
pull_request:
branches:
- master
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
# This step generates the docker tags
- name: Docker meta
id: meta
uses: docker/metadata-action@v4
env:
# This env var ensures {{sha}} is a real commit SHA for type=ref,event=pr
DOCKER_METADATA_PR_HEAD_SHA: 'true'
with:
images: |
djmaze/snappymail
ghcr.io/${{ github.repository }}
# type=ref,event=pr generates tag(s) on PRs only. E.g. 'pr-123', 'pr-123-abc0123'
tags: |
type=ref,event=pr
type=ref,suffix=-{{sha}},event=pr
# The rest of the org.opencontainers.image.xxx labels are dynamically generated
labels: |
org.opencontainers.image.description=SnappyMail
org.opencontainers.image.licenses=AGPLv3
# See: https://github.com/docker/build-push-action/blob/v2.6.1/docs/advanced/cache.md#github-cache
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v2
- name: Cache Docker layers
uses: actions/cache@v3
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-
# See: https://github.com/docker/buildx/issues/59
- name: Build
id: build
uses: docker/build-push-action@v3
with:
context: '.'
file: ./.docker/release/Dockerfile
platforms: linux/amd64
push: false
load: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max
- name: Docker images
run: |
docker images
- name: Test
run: |
TAG=$( echo "${{ steps.meta.outputs.tags }}" | head -n1 )
.docker/release/test/test.sh "$TAG"
- name: Build all archs
uses: docker/build-push-action@v3
with:
context: '.'
file: ./.docker/release/Dockerfile
platforms: linux/386,linux/amd64,linux/arm64
push: false
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max
# Temp fix
# https://github.com/docker/build-push-action/issues/252
# https://github.com/moby/buildkit/issues/1896
- name: Move cache
run: |
rm -rf /tmp/.buildx-cache
mv /tmp/.buildx-cache-new /tmp/.buildx-cache

117
.github/workflows/docker.yml vendored Normal file
View file

@ -0,0 +1,117 @@
name: docker
on:
push:
tags:
- 'v2.*'
# This is needed to push to GitHub Container Registry. See https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry
permissions:
contents: read
packages: write
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
# This step generates the docker tags
- name: Docker meta
id: meta
uses: docker/metadata-action@v4
env:
# This env var ensures {{sha}} is a real commit SHA for type=ref,event=pr
DOCKER_METADATA_PR_HEAD_SHA: 'true'
with:
images: |
djmaze/snappymail
ghcr.io/${{ github.repository }}
# type=ref,event=branch generates tag(s) on branch only. E.g. 'master', 'master-abc0123'
# type=ref,event=tag generates tag(s) on tags only. E.g. 'v0.0.0', 'v0.0.0-abc0123', and 'latest'
tags: |
type=ref,event=branch
type=ref,event=tag
# The rest of the org.opencontainers.image.xxx labels are dynamically generated
labels: |
org.opencontainers.image.description=SnappyMail
org.opencontainers.image.licenses=AGPLv3
# See: https://github.com/docker/build-push-action/blob/v2.6.1/docs/advanced/cache.md#github-cache
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v2
- name: Cache Docker layers
uses: actions/cache@v3
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-
- name: Login to Docker Hub registry
if: startsWith(github.ref, 'refs/tags/') # Login only on tags
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
if: startsWith(github.ref, 'refs/tags/') # Login only on tags
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# See: https://github.com/docker/buildx/issues/59
- name: Build
id: build
uses: docker/build-push-action@v3
with:
context: '.'
file: ./.docker/release/Dockerfile
platforms: linux/amd64
push: false
load: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max
- name: Docker images
run: |
docker images
- name: Test
run: |
TAG=$( echo "${{ steps.meta.outputs.tags }}" | head -n1 )
.docker/release/test/test.sh "$TAG"
- name: Build and push
id: build-and-push
uses: docker/build-push-action@v3
with:
context: '.'
file: ./.docker/release/Dockerfile
# TODO: Add more arches?
# platforms: linux/386,linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/s390x
platforms: linux/386,linux/amd64,linux/arm64
push: ${{ startsWith(github.ref, 'refs/tags/') }} # Push only on tags
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max
# Temp fix
# https://github.com/docker/build-push-action/issues/252
# https://github.com/moby/buildkit/issues/1896
- name: Move cache
run: |
rm -rf /tmp/.buildx-cache
mv /tmp/.buildx-cache-new /tmp/.buildx-cache

3
.gitignore vendored
View file

@ -17,6 +17,7 @@
/data
/MULTIPLY
/include.php
/issues
.idea/
.env
/test
@ -24,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>
@ -26,7 +34,6 @@
# Header set Strict-Transport-Security "max-age=31536000"
Header set imagetoolbar "no"
# Header set X-Content-Type-Options "nosniff"
# Header set X-Frame-Options "DENY"
# Header set X-XSS-Protection "1; mode=block"
Header set Service-Worker-Allowed "/"

File diff suppressed because it is too large Load diff

View file

@ -29,7 +29,7 @@
**Editing HTML Template Files**
1. Edit data/\_data_/\_default_/configs/application.ini
2. Set 'cache_system_data' to Off
2. Set `[cache] system_data` to Off
**Release**

View file

@ -5,6 +5,10 @@
<br>
<h1>SnappyMail</h1>
<br>
[![github-actions](https://github.com/the-djmaze/snappymail/actions/workflows/docker.yml/badge.svg)](https://github.com/the-djmaze/snappymail/actions/workflows/docker.yml)
[![docker-image-size](https://img.shields.io/docker/image-size/djmaze/snappymail/latest)](https://hub.docker.com/r/djmaze/snappymail/tags)
<p>
Simple, modern, lightweight &amp; fast web-based email client.
</p>
@ -26,7 +30,7 @@ For more information about the product, check [snappymail.eu](https://snappymail
Information about installing the product, check the [wiki page](https://github.com/the-djmaze/snappymail/wiki/Installation-instructions).
And don't forget to read the [RainLoop documentation](https://www.rainloop.net/docs/).
And don't forget to read the whole [Wiki](https://github.com/the-djmaze/snappymail/wiki).
## License
@ -34,7 +38,7 @@ And don't forget to read the [RainLoop documentation](https://www.rainloop.net/d
**GNU AFFERO GENERAL PUBLIC LICENSE Version 3 (AGPL)**.
http://www.gnu.org/licenses/agpl-3.0.html
Copyright (c) 2020 - 2022 SnappyMail
Copyright (c) 2020 - 2024 SnappyMail
Copyright (c) 2013 - 2022 RainLoop
## Modifications
@ -45,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
@ -62,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
@ -79,12 +83,7 @@ This fork of RainLoop has the following changes:
* Added [Fetch Metadata Request Headers](https://www.w3.org/TR/fetch-metadata/) checks
* Reduced excessive DOM size
* Support [Kolab groupware](https://kolab.org/)
* Support IMAP RFC 2971 ID extension
* Support IMAP RFC 5258 LIST-EXTENDED
* Support IMAP RFC 5464 METADATA
* Support IMAP RFC 5819 LIST-STATUS
* Support IMAP RFC 7628 SASL OAUTHBEARER aka XOAUTH2
* Support IMAP4rev2 RFC 9051
* Support many more [IMAP RFC's](https://snappymail.eu/comparison#IMAP)
* Support Sodium and OpenSSL for encryption
* Much better PGP support
@ -141,28 +140,28 @@ RainLoop 1.17 vs SnappyMail
|js/* |RainLoop |Snappy |
|--------------- |--------: |--------: |
|admin.js |2.170.153 | 83.917 |
|app.js |4.207.787 | 412.197 |
|boot.js | 868.735 | 2.351 |
|libs.js | 658.812 | 193.682 |
|sieve.js | 0 | 86.018 |
|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 | 778.450 |
|TOTAL |8.240.095 | 861.962 |
|js/min/* |RainLoop |Snappy |RL gzip |SM gzip |RL brotli |SM brotli |
|--------------- |--------: |--------: |------: |------: |--------: |--------: |
|admin.min.js | 256.831 | 41.827 | 73.606 | 13.858 | 60.877 | 12.432 |
|app.min.js | 515.367 | 190.374 |139.456 | 63.219 |110.485 | 54.234 |
|boot.min.js | 84.659 | 1.474 | 26.998 | 907 | 23.643 | 733 |
|libs.min.js | 584.772 | 92.082 |180.901 | 34.144 |155.182 | 30.670 |
|sieve.min.js | 0 | 41.909 | 0 | 10.476 | 0 | 9.461 |
|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 | 283.930 |358.761 | 98.270 |299.485 | 85.637 |
|TOTAL user+sieve |1.217.635 | 325.839 |358.761 |108.746 |299.485 | 95.098 |
|TOTAL admin | 959.099 | 135.383 |292.911 | 48.909 |249.877 | 43.835 |
|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 its around 70% smaller and faster than traditional RainLoop.
For a user it is around 66% smaller and faster than traditional RainLoop.
### CSS changes
@ -189,12 +188,12 @@ For a user its around 70% smaller and faster than traditional RainLoop.
|css/* |RainLoop |Snappy |RL gzip |SM gzip |SM brotli |
|------------ |-------: |------: |------: |------: |--------: |
|app.css | 340.331 | 82.348 | 46.946 | 17.074 | 14.652 |
|app.min.css | 274.947 | 66.376 | 39.647 | 15.048 | 13.200 |
|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.277 | | 6.901 | 5.999 |
|admin.min.css | | 24.524 | | 6.238 | 5.503 |
|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
@ -208,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 | 539.988 |102.388 | 167.371 | 84.241 | 137.413 |
|openpgp.min.js | 330.742 | 546.165 |102.388 | 169.207 | 84.241 | 138.688 |
|openpgp.worker | 1.499 | | 824 | | 695 | |
@ -218,7 +217,6 @@ The [Squire](https://github.com/neilj/Squire) implementation is not 100% compati
Still TODO:
* support for tables (really needed?!?)
* support BIDI (really needed?!?)
| | normal | min | gzip | min gzip |
|-------- |-------: |-------: |------: |--------: |
@ -226,5 +224,3 @@ Still TODO:
|ckeditor | ? | 520.035 | ? | 155.916 |
CKEditor including the 7 asset requests (css,language,plugins,icons) is 633.46 KB / 180.47 KB (gzip).
To use the old CKEditor, you must install the plugin.

View file

@ -6,15 +6,15 @@ Currently due to the fast development only the latest version receives security
| Version | Supported |
| -------- | --------- |
| 2.13.x | ✔ |
| < 2.13.0 | ❌ |
| 2.34.x | ✔ |
| < 2.34.0 | ❌ |
## Reporting a Vulnerability
Please report security issues or vulnerabilities as an encrypted email to [security@snappymail.eu](mailto:security@snappymail.eu).
Your report should be detailed enough with clear steps to reproduce and classify the found vulnerability.
You can find the PGP public key below and on the major public keyservers like [pgp.key-server.io](https://pgp.key-server.io).
You can find the PGP public key below and on the major public keyservers like [keys.openpgp.org](https://keys.openpgp.org).
```
-----BEGIN PGP PUBLIC KEY BLOCK-----
Comment: Type: 255-bit EdDSA
@ -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

@ -4,16 +4,6 @@
//header('Strict-Transport-Security: max-age=31536000');
/**
* Uncomment to use gzip compressed output
*/
//define('USE_GZIP', 1);
/**
* Uncomment to use brotli compressed output
*/
//define('USE_BROTLI', 1);
/**
* Uncomment to enable multiple domain installation.
*/
@ -34,3 +24,8 @@
* Additional configuration file name
*/
//define('APP_CONFIGURATION_NAME', $_SERVER['HTTP_HOST'].'.ini');
/**
* Also update extensions on upgrade
*/
//define('SNAPPYMAIL_UPDATE_PLUGINS', 1);

View file

@ -1,50 +0,0 @@
<IfModule mod_headers.c>
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{HTTP:Accept-encoding} br
RewriteCond "%{REQUEST_FILENAME}\.br" -s
RewriteRule "^(.+\.js)$" "$1\.br" [L,T=application/javascript,QSA]
RewriteCond %{HTTP:Accept-encoding} gzip
RewriteCond "%{REQUEST_FILENAME}\.gz" -s
RewriteRule "^(.+\.js)$" "$1\.gz" [L,T=application/javascript,QSA]
RewriteCond %{HTTP:Accept-encoding} br
RewriteCond "%{REQUEST_FILENAME}\.br" -s
RewriteRule "^(.+\.css)$" "$1\.br" [L,T=text/css,QSA]
RewriteCond %{HTTP:Accept-encoding} gzip
RewriteCond "%{REQUEST_FILENAME}\.gz" -s
RewriteRule "^(.+\.css)$" "$1\.gz" [L,T=text/css,QSA]
<FilesMatch "\.(js|css)\.br$">
SetEnv no-gzip 1
SetEnv no-brotli 1
# Serve correct encoding type.
Header append Content-Encoding br
# Force proxies to cache brotli &
# non-brotli css/js files separately.
Header append Vary Accept-Encoding
</FilesMatch>
<FilesMatch "\.(js|css)\.gz$">
SetEnv no-gzip 1
SetEnv no-brotli 1
# Serve correct encoding type.
Header append Content-Encoding gzip
# Force proxies to cache gzipped &
# non-gzipped css/js files separately.
Header append Vary Accept-Encoding
</FilesMatch>
<FilesMatch "\.js\.(br|gz)$">
Header set Content-Type "application/javascript; charset=utf-8"
ForceType application/javascript
</FilesMatch>
<FilesMatch "\.css\.(br|gz)$">
Header set Content-Type "text/css; charset=utf-8"
ForceType text/css
</FilesMatch>
</IfModule>
</IfModule>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

View file

@ -1,46 +1,92 @@
<?php
echo "\x1b[33;1m === Nextcloud === \x1b[0m\n";
$cert_dir = $_SERVER['HOME'].'/.nextcloud/certificates';
$nc_destination = "{$destPath}snappymail-{$package->version}-nextcloud.tar";
@unlink($nc_destination);
@unlink("{$nc_destination}.gz");
$nc_tar = new PharData($nc_destination);
$hashes = [];
file_put_contents(ROOT_DIR . '/integrations/nextcloud/snappymail/VERSION', $package->version);
$file = ROOT_DIR . '/integrations/nextcloud/snappymail/appinfo/info.xml';
file_put_contents($file, preg_replace('/<version>[^<]*</', "<version>{$package->version}<", file_get_contents($file)));
$nc_tar->buildFromDirectory('./integrations/nextcloud', "@integrations/nextcloud/snappymail/@");
$files = new RecursiveIteratorIterator(new RecursiveDirectoryIterator('integrations/nextcloud/snappymail'));
foreach ($files as $file) {
if (is_file($file)) {
$name = str_replace('\\', '/', $file);
$name = str_replace('integrations/nextcloud/snappymail/', '', $name);
$hashes[$name] = hash_file('sha512', $file);
}
}
$files = new RecursiveIteratorIterator(new RecursiveDirectoryIterator('snappymail/v'), RecursiveIteratorIterator::SELF_FIRST);
foreach ($files as $file) {
if (is_file($file)) {
$newFile = str_replace('\\', '/', $file);
// $newFile = str_replace("'snappymail/v/'.", '', $newFile);
$nc_tar->addFile($file, "snappymail/app/{$newFile}");
$newName = str_replace('/.htaccess', '/_htaccess', $newFile);
$nc_tar->addFile($file, "snappymail/app/{$newName}");
$hashes["app/{$newFile}"] = hash_file('sha512', $file);
}
}
/*
$nc_tar->addFile('data/.htaccess');
$nc_tar->addFromString('data/VERSION', $package->version);
$nc_tar->addFile('data/README.md');
$nc_tar->addFile('_include.php', 'snappymail/app/_include.php');
*/
$nc_tar->addFile('.htaccess', 'snappymail/app/.htaccess');
$nc_tar->addFile('.htaccess', 'snappymail/app/_htaccess');
$hashes['app/.htaccess'] = hash_file('sha512', '.htaccess');
$index = file_get_contents('index.php');
$index = str_replace('0.0.0', $package->version, $index);
//$index = str_replace('snappymail/v/', '', $index);
$nc_tar->addFromString('snappymail/app/index.php', $index);
$hashes['app/index.php'] = hash('sha512', $index);
$nc_tar->addFile('README.md', 'snappymail/app/README.md');
$hashes['app/README.md'] = hash_file('sha512', 'README.md');
$nc_tar->addFile('CHANGELOG.md', 'snappymail/CHANGELOG.md');
$hashes['CHANGELOG.md'] = hash_file('sha512', 'CHANGELOG.md');
$data = file_get_contents('dev/serviceworker.js');
$nc_tar->addFromString('snappymail/app/serviceworker.js', $data);
$hashes['app/serviceworker.js'] = hash('sha512', $data);
spl_autoload_register(function($name){
$file = __DIR__ . '/' . str_replace('\\', '/', $name) . '.php';
require $file;
});
ksort($hashes);
$cert = file_get_contents($cert_dir.'/snappymail.crt');
$rsa = new \phpseclib\Crypt\RSA();
$rsa->loadKey(file_get_contents($cert_dir.'/snappymail.key'));
$x509 = new \phpseclib\File\X509();
$x509->loadX509($cert);
$x509->setPrivateKey($rsa);
$rsa->setSignatureMode(\phpseclib\Crypt\RSA::SIGNATURE_PSS);
$rsa->setMGFHash('sha512');
$rsa->setSaltLength(0);
$signature = $rsa->sign(json_encode($hashes));
$nc_tar->addFromString('snappymail/appinfo/signature.json', json_encode([
'hashes' => $hashes,
'signature' => base64_encode($signature),
'certificate' => $cert
], JSON_PRETTY_PRINT));
$nc_tar->compress(Phar::GZ);
unlink($nc_destination);
$nc_destination .= '.gz';
$signature = shell_exec("openssl dgst -sha512 -sign ~/.nextcloud/certificates/snappymail.key {$nc_destination} | openssl base64");
$signature = shell_exec("openssl dgst -sha512 -sign {$cert_dir}/snappymail.key {$nc_destination} | openssl base64");
file_put_contents($nc_destination.'.sig', $signature);
echo "{$nc_destination} created\n";

100
build/owncloud.php Executable file
View file

@ -0,0 +1,100 @@
<?php
echo "\x1b[33;1m === ownCloud === \x1b[0m\n";
$cert_dir = false; // $_SERVER['HOME'].'/.owncloud/certificates';
$nc_destination = "{$destPath}snappymail-{$package->version}-owncloud.tar";
@unlink($nc_destination);
@unlink("{$nc_destination}.gz");
$nc_tar = new PharData($nc_destination);
$hashes = [];
file_put_contents(ROOT_DIR . '/integrations/owncloud/snappymail/VERSION', $package->version);
$file = ROOT_DIR . '/integrations/owncloud/snappymail/appinfo/info.xml';
file_put_contents($file, preg_replace('/<version>[^<]*</', "<version>{$package->version}<", file_get_contents($file)));
$nc_tar->buildFromDirectory('./integrations/owncloud', "@integrations/owncloud/snappymail/@");
$files = new RecursiveIteratorIterator(new RecursiveDirectoryIterator('integrations/owncloud/snappymail'));
foreach ($files as $file) {
if (is_file($file)) {
$name = str_replace('\\', '/', $file);
$name = str_replace('integrations/owncloud/snappymail/', '', $name);
$hashes[$name] = hash_file('sha512', $file);
}
}
$files = new RecursiveIteratorIterator(new RecursiveDirectoryIterator('snappymail/v'), RecursiveIteratorIterator::SELF_FIRST);
foreach ($files as $file) {
if (is_file($file)) {
$newFile = str_replace('\\', '/', $file);
// $newFile = str_replace("'snappymail/v/'.", '', $newFile);
$nc_tar->addFile($file, "snappymail/app/{$newFile}");
$hashes["app/{$newFile}"] = hash_file('sha512', $file);
}
}
$nc_tar->delete("snappymail/app/snappymail/v/{$package->version}/app/.htaccess");
$nc_tar->addFile("snappymail/v/{$package->version}/app/.htaccess", "snappymail/app/snappymail/v/{$package->version}/app/_htaccess");
$nc_tar->delete("snappymail/app/snappymail/v/{$package->version}/static/.htaccess");
$nc_tar->addFile("snappymail/v/{$package->version}/static/.htaccess", "snappymail/app/snappymail/v/{$package->version}/static/_htaccess");
/*
$nc_tar->addFile('data/.htaccess');
$nc_tar->addFromString('data/VERSION', $package->version);
$nc_tar->addFile('data/README.md');
$nc_tar->addFile('_include.php', 'snappymail/app/_include.php');
*/
$nc_tar->addFile('.htaccess', 'snappymail/app/_htaccess');
$hashes['app/.htaccess'] = hash_file('sha512', '.htaccess');
$index = file_get_contents('index.php');
$index = str_replace('0.0.0', $package->version, $index);
//$index = str_replace('snappymail/v/', '', $index);
$nc_tar->addFromString('snappymail/app/index.php', $index);
$hashes['app/index.php'] = hash('sha512', $index);
$nc_tar->addFile('README.md', 'snappymail/app/README.md');
$hashes['app/README.md'] = hash_file('sha512', 'README.md');
$nc_tar->addFile('CHANGELOG.md', 'snappymail/CHANGELOG.md');
$hashes['CHANGELOG.md'] = hash_file('sha512', 'CHANGELOG.md');
$data = file_get_contents('dev/serviceworker.js');
$nc_tar->addFromString('snappymail/app/serviceworker.js', $data);
$hashes['app/serviceworker.js'] = hash('sha512', $data);
if ($cert_dir) {
spl_autoload_register(function($name){
$file = __DIR__ . '/' . str_replace('\\', '/', $name) . '.php';
require $file;
});
ksort($hashes);
$cert = file_get_contents($cert_dir.'/snappymail.crt');
$rsa = new \phpseclib\Crypt\RSA();
$rsa->loadKey(file_get_contents($cert_dir.'/snappymail.key'));
$x509 = new \phpseclib\File\X509();
$x509->loadX509($cert);
$x509->setPrivateKey($rsa);
$rsa->setSignatureMode(\phpseclib\Crypt\RSA::SIGNATURE_PSS);
$rsa->setMGFHash('sha512');
$rsa->setSaltLength(0);
$signature = $rsa->sign(json_encode($hashes));
$nc_tar->addFromString('snappymail/appinfo/signature.json', json_encode([
'hashes' => $hashes,
'signature' => base64_encode($signature),
'certificate' => $cert
], JSON_PRETTY_PRINT));
}
$nc_tar->compress(Phar::GZ);
unlink($nc_destination);
$nc_destination .= '.gz';
if ($cert_dir) {
$signature = shell_exec("openssl dgst -sha512 -sign {$cert_dir}/snappymail.key {$nc_destination} | openssl base64");
file_put_contents($nc_destination.'.sig', $signature);
}
echo "{$nc_destination} created\n";

View file

@ -0,0 +1,893 @@
<?php
/**
* Pure-PHP implementations of keyed-hash message authentication codes (HMACs) and various cryptographic hashing functions.
*
* Uses hash() or mhash() if available and an internal implementation, otherwise. Currently supports the following:
*
* md2, md5, md5-96, sha1, sha1-96, sha256, sha256-96, sha384, and sha512, sha512-96
*
* If {@link self::setKey() setKey()} is called, {@link self::hash() hash()} will return the HMAC as opposed to
* the hash. If no valid algorithm is provided, sha1 will be used.
*
* PHP version 5
*
* {@internal The variable names are the same as those in
* {@link http://tools.ietf.org/html/rfc2104#section-2 RFC2104}.}}
*
* Here's a short example of how to use this library:
* <code>
* <?php
* include 'vendor/autoload.php';
*
* $hash = new \phpseclib\Crypt\Hash('sha1');
*
* $hash->setKey('abcdefg');
*
* echo base64_encode($hash->hash('abcdefg'));
* ?>
* </code>
*
* @category Crypt
* @package Hash
* @author Jim Wigginton <terrafrost@php.net>
* @copyright 2007 Jim Wigginton
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @link http://phpseclib.sourceforge.net
*/
namespace phpseclib\Crypt;
use phpseclib\Math\BigInteger;
/**
* Pure-PHP implementations of keyed-hash message authentication codes (HMACs) and various cryptographic hashing functions.
*
* @package Hash
* @author Jim Wigginton <terrafrost@php.net>
* @access public
*/
class Hash
{
/**#@+
* @access private
* @see \phpseclib\Crypt\Hash::__construct()
*/
/**
* Toggles the internal implementation
*/
const MODE_INTERNAL = 1;
/**
* Toggles the mhash() implementation, which has been deprecated on PHP 5.3.0+.
*/
const MODE_MHASH = 2;
/**
* Toggles the hash() implementation, which works on PHP 5.1.2+.
*/
const MODE_HASH = 3;
/**#@-*/
/**
* Hash Parameter
*
* @see self::setHash()
* @var int
* @access private
*/
var $hashParam;
/**
* Byte-length of compression blocks / key (Internal HMAC)
*
* @see self::setAlgorithm()
* @var int
* @access private
*/
var $b;
/**
* Byte-length of hash output (Internal HMAC)
*
* @see self::setHash()
* @var int
* @access private
*/
var $l = false;
/**
* Hash Algorithm
*
* @see self::setHash()
* @var string
* @access private
*/
var $hash;
/**
* Key
*
* @see self::setKey()
* @var string
* @access private
*/
var $key = false;
/**
* Computed Key
*
* @see self::_computeKey()
* @var string
* @access private
*/
var $computedKey = false;
/**
* Outer XOR (Internal HMAC)
*
* @see self::setKey()
* @var string
* @access private
*/
var $opad;
/**
* Inner XOR (Internal HMAC)
*
* @see self::setKey()
* @var string
* @access private
*/
var $ipad;
/**
* Engine
*
* @see self::setHash()
* @var string
* @access private
*/
var $engine;
/**
* Default Constructor.
*
* @param string $hash
* @return \phpseclib\Crypt\Hash
* @access public
*/
function __construct($hash = 'sha1')
{
if (!defined('CRYPT_HASH_MODE')) {
switch (true) {
case extension_loaded('hash'):
define('CRYPT_HASH_MODE', self::MODE_HASH);
break;
case extension_loaded('mhash'):
define('CRYPT_HASH_MODE', self::MODE_MHASH);
break;
default:
define('CRYPT_HASH_MODE', self::MODE_INTERNAL);
}
}
$this->setHash($hash);
}
/**
* Sets the key for HMACs
*
* Keys can be of any length.
*
* @access public
* @param string $key
*/
function setKey($key = false)
{
$this->key = $key;
$this->_computeKey();
}
/**
* Pre-compute the key used by the HMAC
*
* Quoting http://tools.ietf.org/html/rfc2104#section-2, "Applications that use keys longer than B bytes
* will first hash the key using H and then use the resultant L byte string as the actual key to HMAC."
*
* As documented in https://www.reddit.com/r/PHP/comments/9nct2l/symfonypolyfill_hash_pbkdf2_correct_fix_for/
* when doing an HMAC multiple times it's faster to compute the hash once instead of computing it during
* every call
*
* @access private
*/
function _computeKey()
{
if ($this->key === false) {
$this->computedKey = false;
return;
}
if (strlen($this->key) <= $this->b) {
$this->computedKey = $this->key;
return;
}
switch ($this->engine) {
case self::MODE_MHASH:
$this->computedKey = mhash($this->hash, $this->key);
break;
case self::MODE_HASH:
$this->computedKey = hash($this->hash, $this->key, true);
break;
case self::MODE_INTERNAL:
$this->computedKey = call_user_func($this->hash, $this->key);
}
}
/**
* Gets the hash function.
*
* As set by the constructor or by the setHash() method.
*
* @access public
* @return string
*/
function getHash()
{
return $this->hashParam;
}
/**
* Sets the hash function.
*
* @access public
* @param string $hash
*/
function setHash($hash)
{
$this->hashParam = $hash = strtolower($hash);
switch ($hash) {
case 'md5-96':
case 'sha1-96':
case 'sha256-96':
case 'sha512-96':
$hash = substr($hash, 0, -3);
$this->l = 12; // 96 / 8 = 12
break;
case 'md2':
case 'md5':
$this->l = 16;
break;
case 'sha1':
$this->l = 20;
break;
case 'sha256':
$this->l = 32;
break;
case 'sha384':
$this->l = 48;
break;
case 'sha512':
$this->l = 64;
}
switch ($hash) {
case 'md2-96':
case 'md2':
$this->b = 16;
case 'md5-96':
case 'sha1-96':
case 'sha224-96':
case 'sha256-96':
case 'md2':
case 'md5':
case 'sha1':
case 'sha224':
case 'sha256':
$this->b = 64;
break;
default:
$this->b = 128;
}
switch ($hash) {
case 'md2':
$this->engine = CRYPT_HASH_MODE == self::MODE_HASH && in_array('md2', hash_algos()) ?
self::MODE_HASH : self::MODE_INTERNAL;
break;
case 'sha384':
case 'sha512':
$this->engine = CRYPT_HASH_MODE == self::MODE_MHASH ? self::MODE_INTERNAL : CRYPT_HASH_MODE;
break;
default:
$this->engine = CRYPT_HASH_MODE;
}
switch ($this->engine) {
case self::MODE_MHASH:
switch ($hash) {
case 'md5':
$this->hash = MHASH_MD5;
break;
case 'sha256':
$this->hash = MHASH_SHA256;
break;
case 'sha1':
default:
$this->hash = MHASH_SHA1;
}
$this->_computeKey(self::MODE_MHASH);
return;
case self::MODE_HASH:
switch ($hash) {
case 'md5':
$this->hash = 'md5';
return;
case 'md2':
case 'sha256':
case 'sha384':
case 'sha512':
$this->hash = $hash;
return;
case 'sha1':
default:
$this->hash = 'sha1';
}
$this->_computeKey(self::MODE_HASH);
return;
}
switch ($hash) {
case 'md2':
$this->hash = array($this, '_md2');
break;
case 'md5':
$this->hash = array($this, '_md5');
break;
case 'sha256':
$this->hash = array($this, '_sha256');
break;
case 'sha384':
case 'sha512':
$this->hash = array($this, '_sha512');
break;
case 'sha1':
default:
$this->hash = array($this, '_sha1');
}
$this->ipad = str_repeat(chr(0x36), $this->b);
$this->opad = str_repeat(chr(0x5C), $this->b);
$this->_computeKey(self::MODE_INTERNAL);
}
/**
* Compute the HMAC.
*
* @access public
* @param string $text
* @return string
*/
function hash($text)
{
if (!empty($this->key) || is_string($this->key)) {
switch ($this->engine) {
case self::MODE_MHASH:
$output = mhash($this->hash, $text, $this->computedKey);
break;
case self::MODE_HASH:
$output = hash_hmac($this->hash, $text, $this->computedKey, true);
break;
case self::MODE_INTERNAL:
$key = str_pad($this->computedKey, $this->b, chr(0)); // step 1
$temp = $this->ipad ^ $key; // step 2
$temp .= $text; // step 3
$temp = call_user_func($this->hash, $temp); // step 4
$output = $this->opad ^ $key; // step 5
$output.= $temp; // step 6
$output = call_user_func($this->hash, $output); // step 7
}
} else {
switch ($this->engine) {
case self::MODE_MHASH:
$output = mhash($this->hash, $text);
break;
case self::MODE_HASH:
$output = hash($this->hash, $text, true);
break;
case self::MODE_INTERNAL:
$output = call_user_func($this->hash, $text);
}
}
return substr($output, 0, $this->l);
}
/**
* Returns the hash length (in bytes)
*
* @access public
* @return int
*/
function getLength()
{
return $this->l;
}
/**
* Wrapper for MD5
*
* @access private
* @param string $m
*/
function _md5($m)
{
return pack('H*', md5($m));
}
/**
* Wrapper for SHA1
*
* @access private
* @param string $m
*/
function _sha1($m)
{
return pack('H*', sha1($m));
}
/**
* Pure-PHP implementation of MD2
*
* See {@link http://tools.ietf.org/html/rfc1319 RFC1319}.
*
* @access private
* @param string $m
*/
function _md2($m)
{
static $s = array(
41, 46, 67, 201, 162, 216, 124, 1, 61, 54, 84, 161, 236, 240, 6,
19, 98, 167, 5, 243, 192, 199, 115, 140, 152, 147, 43, 217, 188,
76, 130, 202, 30, 155, 87, 60, 253, 212, 224, 22, 103, 66, 111, 24,
138, 23, 229, 18, 190, 78, 196, 214, 218, 158, 222, 73, 160, 251,
245, 142, 187, 47, 238, 122, 169, 104, 121, 145, 21, 178, 7, 63,
148, 194, 16, 137, 11, 34, 95, 33, 128, 127, 93, 154, 90, 144, 50,
39, 53, 62, 204, 231, 191, 247, 151, 3, 255, 25, 48, 179, 72, 165,
181, 209, 215, 94, 146, 42, 172, 86, 170, 198, 79, 184, 56, 210,
150, 164, 125, 182, 118, 252, 107, 226, 156, 116, 4, 241, 69, 157,
112, 89, 100, 113, 135, 32, 134, 91, 207, 101, 230, 45, 168, 2, 27,
96, 37, 173, 174, 176, 185, 246, 28, 70, 97, 105, 52, 64, 126, 15,
85, 71, 163, 35, 221, 81, 175, 58, 195, 92, 249, 206, 186, 197,
234, 38, 44, 83, 13, 110, 133, 40, 132, 9, 211, 223, 205, 244, 65,
129, 77, 82, 106, 220, 55, 200, 108, 193, 171, 250, 36, 225, 123,
8, 12, 189, 177, 74, 120, 136, 149, 139, 227, 99, 232, 109, 233,
203, 213, 254, 59, 0, 29, 57, 242, 239, 183, 14, 102, 88, 208, 228,
166, 119, 114, 248, 235, 117, 75, 10, 49, 68, 80, 180, 143, 237,
31, 26, 219, 153, 141, 51, 159, 17, 131, 20
);
// Step 1. Append Padding Bytes
$pad = 16 - (strlen($m) & 0xF);
$m.= str_repeat(chr($pad), $pad);
$length = strlen($m);
// Step 2. Append Checksum
$c = str_repeat(chr(0), 16);
$l = chr(0);
for ($i = 0; $i < $length; $i+= 16) {
for ($j = 0; $j < 16; $j++) {
// RFC1319 incorrectly states that C[j] should be set to S[c xor L]
//$c[$j] = chr($s[ord($m[$i + $j] ^ $l)]);
// per <http://www.rfc-editor.org/errata_search.php?rfc=1319>, however, C[j] should be set to S[c xor L] xor C[j]
$c[$j] = chr($s[ord($m[$i + $j] ^ $l)] ^ ord($c[$j]));
$l = $c[$j];
}
}
$m.= $c;
$length+= 16;
// Step 3. Initialize MD Buffer
$x = str_repeat(chr(0), 48);
// Step 4. Process Message in 16-Byte Blocks
for ($i = 0; $i < $length; $i+= 16) {
for ($j = 0; $j < 16; $j++) {
$x[$j + 16] = $m[$i + $j];
$x[$j + 32] = $x[$j + 16] ^ $x[$j];
}
$t = chr(0);
for ($j = 0; $j < 18; $j++) {
for ($k = 0; $k < 48; $k++) {
$x[$k] = $t = $x[$k] ^ chr($s[ord($t)]);
//$t = $x[$k] = $x[$k] ^ chr($s[ord($t)]);
}
$t = chr(ord($t) + $j);
}
}
// Step 5. Output
return substr($x, 0, 16);
}
/**
* Pure-PHP implementation of SHA256
*
* See {@link http://en.wikipedia.org/wiki/SHA_hash_functions#SHA-256_.28a_SHA-2_variant.29_pseudocode SHA-256 (a SHA-2 variant) pseudocode - Wikipedia}.
*
* @access private
* @param string $m
*/
function _sha256($m)
{
if (extension_loaded('suhosin')) {
return pack('H*', sha256($m));
}
// Initialize variables
$hash = array(
0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19
);
// Initialize table of round constants
// (first 32 bits of the fractional parts of the cube roots of the first 64 primes 2..311)
static $k = array(
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2
);
// Pre-processing
$length = strlen($m);
// to round to nearest 56 mod 64, we'll add 64 - (length + (64 - 56)) % 64
$m.= str_repeat(chr(0), 64 - (($length + 8) & 0x3F));
$m[$length] = chr(0x80);
// we don't support hashing strings 512MB long
$m.= pack('N2', 0, $length << 3);
// Process the message in successive 512-bit chunks
$chunks = str_split($m, 64);
foreach ($chunks as $chunk) {
$w = array();
for ($i = 0; $i < 16; $i++) {
extract(unpack('Ntemp', $this->_string_shift($chunk, 4)));
$w[] = $temp;
}
// Extend the sixteen 32-bit words into sixty-four 32-bit words
for ($i = 16; $i < 64; $i++) {
// @codingStandardsIgnoreStart
$s0 = $this->_rightRotate($w[$i - 15], 7) ^
$this->_rightRotate($w[$i - 15], 18) ^
$this->_rightShift( $w[$i - 15], 3);
$s1 = $this->_rightRotate($w[$i - 2], 17) ^
$this->_rightRotate($w[$i - 2], 19) ^
$this->_rightShift( $w[$i - 2], 10);
// @codingStandardsIgnoreEnd
$w[$i] = $this->_add($w[$i - 16], $s0, $w[$i - 7], $s1);
}
// Initialize hash value for this chunk
list($a, $b, $c, $d, $e, $f, $g, $h) = $hash;
// Main loop
for ($i = 0; $i < 64; $i++) {
$s0 = $this->_rightRotate($a, 2) ^
$this->_rightRotate($a, 13) ^
$this->_rightRotate($a, 22);
$maj = ($a & $b) ^
($a & $c) ^
($b & $c);
$t2 = $this->_add($s0, $maj);
$s1 = $this->_rightRotate($e, 6) ^
$this->_rightRotate($e, 11) ^
$this->_rightRotate($e, 25);
$ch = ($e & $f) ^
($this->_not($e) & $g);
$t1 = $this->_add($h, $s1, $ch, $k[$i], $w[$i]);
$h = $g;
$g = $f;
$f = $e;
$e = $this->_add($d, $t1);
$d = $c;
$c = $b;
$b = $a;
$a = $this->_add($t1, $t2);
}
// Add this chunk's hash to result so far
$hash = array(
$this->_add($hash[0], $a),
$this->_add($hash[1], $b),
$this->_add($hash[2], $c),
$this->_add($hash[3], $d),
$this->_add($hash[4], $e),
$this->_add($hash[5], $f),
$this->_add($hash[6], $g),
$this->_add($hash[7], $h)
);
}
// Produce the final hash value (big-endian)
return pack('N8', $hash[0], $hash[1], $hash[2], $hash[3], $hash[4], $hash[5], $hash[6], $hash[7]);
}
/**
* Pure-PHP implementation of SHA384 and SHA512
*
* @access private
* @param string $m
*/
function _sha512($m)
{
static $init384, $init512, $k;
if (!isset($k)) {
// Initialize variables
$init384 = array( // initial values for SHA384
'cbbb9d5dc1059ed8', '629a292a367cd507', '9159015a3070dd17', '152fecd8f70e5939',
'67332667ffc00b31', '8eb44a8768581511', 'db0c2e0d64f98fa7', '47b5481dbefa4fa4'
);
$init512 = array( // initial values for SHA512
'6a09e667f3bcc908', 'bb67ae8584caa73b', '3c6ef372fe94f82b', 'a54ff53a5f1d36f1',
'510e527fade682d1', '9b05688c2b3e6c1f', '1f83d9abfb41bd6b', '5be0cd19137e2179'
);
for ($i = 0; $i < 8; $i++) {
$init384[$i] = new BigInteger($init384[$i], 16);
$init384[$i]->setPrecision(64);
$init512[$i] = new BigInteger($init512[$i], 16);
$init512[$i]->setPrecision(64);
}
// Initialize table of round constants
// (first 64 bits of the fractional parts of the cube roots of the first 80 primes 2..409)
$k = array(
'428a2f98d728ae22', '7137449123ef65cd', 'b5c0fbcfec4d3b2f', 'e9b5dba58189dbbc',
'3956c25bf348b538', '59f111f1b605d019', '923f82a4af194f9b', 'ab1c5ed5da6d8118',
'd807aa98a3030242', '12835b0145706fbe', '243185be4ee4b28c', '550c7dc3d5ffb4e2',
'72be5d74f27b896f', '80deb1fe3b1696b1', '9bdc06a725c71235', 'c19bf174cf692694',
'e49b69c19ef14ad2', 'efbe4786384f25e3', '0fc19dc68b8cd5b5', '240ca1cc77ac9c65',
'2de92c6f592b0275', '4a7484aa6ea6e483', '5cb0a9dcbd41fbd4', '76f988da831153b5',
'983e5152ee66dfab', 'a831c66d2db43210', 'b00327c898fb213f', 'bf597fc7beef0ee4',
'c6e00bf33da88fc2', 'd5a79147930aa725', '06ca6351e003826f', '142929670a0e6e70',
'27b70a8546d22ffc', '2e1b21385c26c926', '4d2c6dfc5ac42aed', '53380d139d95b3df',
'650a73548baf63de', '766a0abb3c77b2a8', '81c2c92e47edaee6', '92722c851482353b',
'a2bfe8a14cf10364', 'a81a664bbc423001', 'c24b8b70d0f89791', 'c76c51a30654be30',
'd192e819d6ef5218', 'd69906245565a910', 'f40e35855771202a', '106aa07032bbd1b8',
'19a4c116b8d2d0c8', '1e376c085141ab53', '2748774cdf8eeb99', '34b0bcb5e19b48a8',
'391c0cb3c5c95a63', '4ed8aa4ae3418acb', '5b9cca4f7763e373', '682e6ff3d6b2b8a3',
'748f82ee5defb2fc', '78a5636f43172f60', '84c87814a1f0ab72', '8cc702081a6439ec',
'90befffa23631e28', 'a4506cebde82bde9', 'bef9a3f7b2c67915', 'c67178f2e372532b',
'ca273eceea26619c', 'd186b8c721c0c207', 'eada7dd6cde0eb1e', 'f57d4f7fee6ed178',
'06f067aa72176fba', '0a637dc5a2c898a6', '113f9804bef90dae', '1b710b35131c471b',
'28db77f523047d84', '32caab7b40c72493', '3c9ebe0a15c9bebc', '431d67c49c100d4c',
'4cc5d4becb3e42b6', '597f299cfc657e2a', '5fcb6fab3ad6faec', '6c44198c4a475817'
);
for ($i = 0; $i < 80; $i++) {
$k[$i] = new BigInteger($k[$i], 16);
}
}
$hash = $this->l == 48 ? $init384 : $init512;
// Pre-processing
$length = strlen($m);
// to round to nearest 112 mod 128, we'll add 128 - (length + (128 - 112)) % 128
$m.= str_repeat(chr(0), 128 - (($length + 16) & 0x7F));
$m[$length] = chr(0x80);
// we don't support hashing strings 512MB long
$m.= pack('N4', 0, 0, 0, $length << 3);
// Process the message in successive 1024-bit chunks
$chunks = str_split($m, 128);
foreach ($chunks as $chunk) {
$w = array();
for ($i = 0; $i < 16; $i++) {
$temp = new BigInteger($this->_string_shift($chunk, 8), 256);
$temp->setPrecision(64);
$w[] = $temp;
}
// Extend the sixteen 32-bit words into eighty 32-bit words
for ($i = 16; $i < 80; $i++) {
$temp = array(
$w[$i - 15]->bitwise_rightRotate(1),
$w[$i - 15]->bitwise_rightRotate(8),
$w[$i - 15]->bitwise_rightShift(7)
);
$s0 = $temp[0]->bitwise_xor($temp[1]);
$s0 = $s0->bitwise_xor($temp[2]);
$temp = array(
$w[$i - 2]->bitwise_rightRotate(19),
$w[$i - 2]->bitwise_rightRotate(61),
$w[$i - 2]->bitwise_rightShift(6)
);
$s1 = $temp[0]->bitwise_xor($temp[1]);
$s1 = $s1->bitwise_xor($temp[2]);
$w[$i] = $w[$i - 16]->copy();
$w[$i] = $w[$i]->add($s0);
$w[$i] = $w[$i]->add($w[$i - 7]);
$w[$i] = $w[$i]->add($s1);
}
// Initialize hash value for this chunk
$a = $hash[0]->copy();
$b = $hash[1]->copy();
$c = $hash[2]->copy();
$d = $hash[3]->copy();
$e = $hash[4]->copy();
$f = $hash[5]->copy();
$g = $hash[6]->copy();
$h = $hash[7]->copy();
// Main loop
for ($i = 0; $i < 80; $i++) {
$temp = array(
$a->bitwise_rightRotate(28),
$a->bitwise_rightRotate(34),
$a->bitwise_rightRotate(39)
);
$s0 = $temp[0]->bitwise_xor($temp[1]);
$s0 = $s0->bitwise_xor($temp[2]);
$temp = array(
$a->bitwise_and($b),
$a->bitwise_and($c),
$b->bitwise_and($c)
);
$maj = $temp[0]->bitwise_xor($temp[1]);
$maj = $maj->bitwise_xor($temp[2]);
$t2 = $s0->add($maj);
$temp = array(
$e->bitwise_rightRotate(14),
$e->bitwise_rightRotate(18),
$e->bitwise_rightRotate(41)
);
$s1 = $temp[0]->bitwise_xor($temp[1]);
$s1 = $s1->bitwise_xor($temp[2]);
$temp = array(
$e->bitwise_and($f),
$g->bitwise_and($e->bitwise_not())
);
$ch = $temp[0]->bitwise_xor($temp[1]);
$t1 = $h->add($s1);
$t1 = $t1->add($ch);
$t1 = $t1->add($k[$i]);
$t1 = $t1->add($w[$i]);
$h = $g->copy();
$g = $f->copy();
$f = $e->copy();
$e = $d->add($t1);
$d = $c->copy();
$c = $b->copy();
$b = $a->copy();
$a = $t1->add($t2);
}
// Add this chunk's hash to result so far
$hash = array(
$hash[0]->add($a),
$hash[1]->add($b),
$hash[2]->add($c),
$hash[3]->add($d),
$hash[4]->add($e),
$hash[5]->add($f),
$hash[6]->add($g),
$hash[7]->add($h)
);
}
// Produce the final hash value (big-endian)
// (\phpseclib\Crypt\Hash::hash() trims the output for hashes but not for HMACs. as such, we trim the output here)
$temp = $hash[0]->toBytes() . $hash[1]->toBytes() . $hash[2]->toBytes() . $hash[3]->toBytes() .
$hash[4]->toBytes() . $hash[5]->toBytes();
if ($this->l != 48) {
$temp.= $hash[6]->toBytes() . $hash[7]->toBytes();
}
return $temp;
}
/**
* Right Rotate
*
* @access private
* @param int $int
* @param int $amt
* @see self::_sha256()
* @return int
*/
function _rightRotate($int, $amt)
{
$invamt = 32 - $amt;
$mask = (1 << $invamt) - 1;
return (($int << $invamt) & 0xFFFFFFFF) | (($int >> $amt) & $mask);
}
/**
* Right Shift
*
* @access private
* @param int $int
* @param int $amt
* @see self::_sha256()
* @return int
*/
function _rightShift($int, $amt)
{
$mask = (1 << (32 - $amt)) - 1;
return ($int >> $amt) & $mask;
}
/**
* Not
*
* @access private
* @param int $int
* @see self::_sha256()
* @return int
*/
function _not($int)
{
return ~$int & 0xFFFFFFFF;
}
/**
* Add
*
* _sha256() adds multiple unsigned 32-bit integers. Since PHP doesn't support unsigned integers and since the
* possibility of overflow exists, care has to be taken. BigInteger could be used but this should be faster.
*
* @return int
* @see self::_sha256()
* @access private
*/
function _add()
{
static $mod;
if (!isset($mod)) {
$mod = pow(2, 32);
}
$result = 0;
$arguments = func_get_args();
foreach ($arguments as $argument) {
$result+= $argument < 0 ? ($argument & 0x7FFFFFFF) + 0x80000000 : $argument;
}
if ((php_uname('m') & "\xDF\xDF\xDF") != 'ARM') {
return fmod($result, $mod);
}
return (fmod($result, 0x80000000) & 0x7FFFFFFF) |
((fmod(floor($result / 0x80000000), 2) & 1) << 31);
}
/**
* String Shift
*
* Inspired by array_shift
*
* @param string $string
* @param int $index
* @return string
* @access private
*/
function _string_shift(&$string, $index = 1)
{
$substr = substr($string, 0, $index);
$string = substr($string, $index);
return $substr;
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,277 @@
<?php
/**
* Random Number Generator
*
* PHP version 5
*
* Here's a short example of how to use this library:
* <code>
* <?php
* include 'vendor/autoload.php';
*
* echo bin2hex(\phpseclib\Crypt\Random::string(8));
* ?>
* </code>
*
* @category Crypt
* @package Random
* @author Jim Wigginton <terrafrost@php.net>
* @copyright 2007 Jim Wigginton
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @link http://phpseclib.sourceforge.net
*/
namespace phpseclib\Crypt;
/**
* Pure-PHP Random Number Generator
*
* @package Random
* @author Jim Wigginton <terrafrost@php.net>
* @access public
*/
class Random
{
/**
* Generate a random string.
*
* Although microoptimizations are generally discouraged as they impair readability this function is ripe with
* microoptimizations because this function has the potential of being called a huge number of times.
* eg. for RSA key generation.
*
* @param int $length
* @return string
*/
static function string($length)
{
if (!$length) {
return '';
}
if (version_compare(PHP_VERSION, '7.0.0', '>=')) {
try {
return \random_bytes($length);
} catch (\Throwable $e) {
// If a sufficient source of randomness is unavailable, random_bytes() will throw an
// object that implements the Throwable interface (Exception, TypeError, Error).
// We don't actually need to do anything here. The string() method should just continue
// as normal. Note, however, that if we don't have a sufficient source of randomness for
// random_bytes(), most of the other calls here will fail too, so we'll end up using
// the PHP implementation.
}
}
if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
// method 1. prior to PHP 5.3 this would call rand() on windows hence the function_exists('class_alias') call.
// ie. class_alias is a function that was introduced in PHP 5.3
if (extension_loaded('mcrypt') && function_exists('class_alias')) {
return @mcrypt_create_iv($length);
}
// method 2. openssl_random_pseudo_bytes was introduced in PHP 5.3.0 but prior to PHP 5.3.4 there was,
// to quote <http://php.net/ChangeLog-5.php#5.3.4>, "possible blocking behavior". as of 5.3.4
// openssl_random_pseudo_bytes and mcrypt_create_iv do the exact same thing on Windows. ie. they both
// call php_win32_get_random_bytes():
//
// https://github.com/php/php-src/blob/7014a0eb6d1611151a286c0ff4f2238f92c120d6/ext/openssl/openssl.c#L5008
// https://github.com/php/php-src/blob/7014a0eb6d1611151a286c0ff4f2238f92c120d6/ext/mcrypt/mcrypt.c#L1392
//
// php_win32_get_random_bytes() is defined thusly:
//
// https://github.com/php/php-src/blob/7014a0eb6d1611151a286c0ff4f2238f92c120d6/win32/winutil.c#L80
//
// we're calling it, all the same, in the off chance that the mcrypt extension is not available
if (extension_loaded('openssl') && version_compare(PHP_VERSION, '5.3.4', '>=')) {
return openssl_random_pseudo_bytes($length);
}
} else {
// method 1. the fastest
if (extension_loaded('openssl')) {
return openssl_random_pseudo_bytes($length);
}
// method 2
static $fp = true;
if ($fp === true) {
// warning's will be output unles the error suppression operator is used. errors such as
// "open_basedir restriction in effect", "Permission denied", "No such file or directory", etc.
$fp = @fopen('/dev/urandom', 'rb');
}
if ($fp !== true && $fp !== false) { // surprisingly faster than !is_bool() or is_resource()
$temp = fread($fp, $length);
if (strlen($temp) == $length) {
return $temp;
}
}
// method 3. pretty much does the same thing as method 2 per the following url:
// https://github.com/php/php-src/blob/7014a0eb6d1611151a286c0ff4f2238f92c120d6/ext/mcrypt/mcrypt.c#L1391
// surprisingly slower than method 2. maybe that's because mcrypt_create_iv does a bunch of error checking that we're
// not doing. regardless, this'll only be called if this PHP script couldn't open /dev/urandom due to open_basedir
// restrictions or some such
if (extension_loaded('mcrypt')) {
return @mcrypt_create_iv($length, MCRYPT_DEV_URANDOM);
}
}
// at this point we have no choice but to use a pure-PHP CSPRNG
// cascade entropy across multiple PHP instances by fixing the session and collecting all
// environmental variables, including the previous session data and the current session
// data.
//
// mt_rand seeds itself by looking at the PID and the time, both of which are (relatively)
// easy to guess at. linux uses mouse clicks, keyboard timings, etc, as entropy sources, but
// PHP isn't low level to be able to use those as sources and on a web server there's not likely
// going to be a ton of keyboard or mouse action. web servers do have one thing that we can use
// however, a ton of people visiting the website. obviously you don't want to base your seeding
// soley on parameters a potential attacker sends but (1) not everything in $_SERVER is controlled
// by the user and (2) this isn't just looking at the data sent by the current user - it's based
// on the data sent by all users. one user requests the page and a hash of their info is saved.
// another user visits the page and the serialization of their data is utilized along with the
// server envirnment stuff and a hash of the previous http request data (which itself utilizes
// a hash of the session data before that). certainly an attacker should be assumed to have
// full control over his own http requests. he, however, is not going to have control over
// everyone's http requests.
static $crypto = false, $v;
if ($crypto === false) {
// save old session data
$old_session_id = session_id();
$old_use_cookies = ini_get('session.use_cookies');
$old_session_cache_limiter = session_cache_limiter();
$_OLD_SESSION = isset($_SESSION) ? $_SESSION : false;
if ($old_session_id != '') {
session_write_close();
}
session_id(1);
ini_set('session.use_cookies', 0);
session_cache_limiter('');
session_start();
$v = $seed = $_SESSION['seed'] = pack('H*', sha1(
(isset($_SERVER) ? phpseclib_safe_serialize($_SERVER) : '') .
(isset($_POST) ? phpseclib_safe_serialize($_POST) : '') .
(isset($_GET) ? phpseclib_safe_serialize($_GET) : '') .
(isset($_COOKIE) ? phpseclib_safe_serialize($_COOKIE) : '') .
phpseclib_safe_serialize($GLOBALS) .
phpseclib_safe_serialize($_SESSION) .
phpseclib_safe_serialize($_OLD_SESSION)
));
if (!isset($_SESSION['count'])) {
$_SESSION['count'] = 0;
}
$_SESSION['count']++;
session_write_close();
// restore old session data
if ($old_session_id != '') {
session_id($old_session_id);
session_start();
ini_set('session.use_cookies', $old_use_cookies);
session_cache_limiter($old_session_cache_limiter);
} else {
if ($_OLD_SESSION !== false) {
$_SESSION = $_OLD_SESSION;
unset($_OLD_SESSION);
} else {
unset($_SESSION);
}
}
// in SSH2 a shared secret and an exchange hash are generated through the key exchange process.
// the IV client to server is the hash of that "nonce" with the letter A and for the encryption key it's the letter C.
// if the hash doesn't produce enough a key or an IV that's long enough concat successive hashes of the
// original hash and the current hash. we'll be emulating that. for more info see the following URL:
//
// http://tools.ietf.org/html/rfc4253#section-7.2
//
// see the is_string($crypto) part for an example of how to expand the keys
$key = pack('H*', sha1($seed . 'A'));
$iv = pack('H*', sha1($seed . 'C'));
// ciphers are used as per the nist.gov link below. also, see this link:
//
// http://en.wikipedia.org/wiki/Cryptographically_secure_pseudorandom_number_generator#Designs_based_on_cryptographic_primitives
switch (true) {
case class_exists('\phpseclib\Crypt\AES'):
$crypto = new AES(Base::MODE_CTR);
break;
case class_exists('\phpseclib\Crypt\Twofish'):
$crypto = new Twofish(Base::MODE_CTR);
break;
case class_exists('\phpseclib\Crypt\Blowfish'):
$crypto = new Blowfish(Base::MODE_CTR);
break;
case class_exists('\phpseclib\Crypt\TripleDES'):
$crypto = new TripleDES(Base::MODE_CTR);
break;
case class_exists('\phpseclib\Crypt\DES'):
$crypto = new DES(Base::MODE_CTR);
break;
case class_exists('\phpseclib\Crypt\RC4'):
$crypto = new RC4();
break;
default:
user_error(__CLASS__ . ' requires at least one symmetric cipher be loaded');
return false;
}
$crypto->setKey($key);
$crypto->setIV($iv);
$crypto->enableContinuousBuffer();
}
//return $crypto->encrypt(str_repeat("\0", $length));
// the following is based off of ANSI X9.31:
//
// http://csrc.nist.gov/groups/STM/cavp/documents/rng/931rngext.pdf
//
// OpenSSL uses that same standard for it's random numbers:
//
// http://www.opensource.apple.com/source/OpenSSL/OpenSSL-38/openssl/fips-1.0/rand/fips_rand.c
// (do a search for "ANS X9.31 A.2.4")
$result = '';
while (strlen($result) < $length) {
$i = $crypto->encrypt(microtime()); // strlen(microtime()) == 21
$r = $crypto->encrypt($i ^ $v); // strlen($v) == 20
$v = $crypto->encrypt($r ^ $i); // strlen($r) == 20
$result.= $r;
}
return substr($result, 0, $length);
}
}
if (!function_exists('phpseclib_safe_serialize')) {
/**
* Safely serialize variables
*
* If a class has a private __sleep() method it'll give a fatal error on PHP 5.2 and earlier.
* PHP 5.3 will emit a warning.
*
* @param mixed $arr
* @access public
*/
function phpseclib_safe_serialize(&$arr)
{
if (is_object($arr)) {
return '';
}
if (!is_array($arr)) {
return serialize($arr);
}
// prevent circular array recursion
if (isset($arr['__phpseclib_marker'])) {
return '';
}
$safearr = array();
$arr['__phpseclib_marker'] = true;
foreach (array_keys($arr) as $key) {
// do not recurse on the '__phpseclib_marker' key itself, for smaller memory usage
if ($key !== '__phpseclib_marker') {
$safearr[$key] = phpseclib_safe_serialize($arr[$key]);
}
}
unset($arr['__phpseclib_marker']);
return serialize($safearr);
}
}

View file

@ -0,0 +1,577 @@
<?php
/**
* Pure-PHP ANSI Decoder
*
* PHP version 5
*
* If you call read() in \phpseclib\Net\SSH2 you may get {@link http://en.wikipedia.org/wiki/ANSI_escape_code ANSI escape codes} back.
* They'd look like chr(0x1B) . '[00m' or whatever (0x1B = ESC). They tell a
* {@link http://en.wikipedia.org/wiki/Terminal_emulator terminal emulator} how to format the characters, what
* color to display them in, etc. \phpseclib\File\ANSI is a {@link http://en.wikipedia.org/wiki/VT100 VT100} terminal emulator.
*
* @category File
* @package ANSI
* @author Jim Wigginton <terrafrost@php.net>
* @copyright 2012 Jim Wigginton
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @link http://phpseclib.sourceforge.net
*/
namespace phpseclib\File;
/**
* Pure-PHP ANSI Decoder
*
* @package ANSI
* @author Jim Wigginton <terrafrost@php.net>
* @access public
*/
class ANSI
{
/**
* Max Width
*
* @var int
* @access private
*/
var $max_x;
/**
* Max Height
*
* @var int
* @access private
*/
var $max_y;
/**
* Max History
*
* @var int
* @access private
*/
var $max_history;
/**
* History
*
* @var array
* @access private
*/
var $history;
/**
* History Attributes
*
* @var array
* @access private
*/
var $history_attrs;
/**
* Current Column
*
* @var int
* @access private
*/
var $x;
/**
* Current Row
*
* @var int
* @access private
*/
var $y;
/**
* Old Column
*
* @var int
* @access private
*/
var $old_x;
/**
* Old Row
*
* @var int
* @access private
*/
var $old_y;
/**
* An empty attribute cell
*
* @var object
* @access private
*/
var $base_attr_cell;
/**
* The current attribute cell
*
* @var object
* @access private
*/
var $attr_cell;
/**
* An empty attribute row
*
* @var array
* @access private
*/
var $attr_row;
/**
* The current screen text
*
* @var array
* @access private
*/
var $screen;
/**
* The current screen attributes
*
* @var array
* @access private
*/
var $attrs;
/**
* Current ANSI code
*
* @var string
* @access private
*/
var $ansi;
/**
* Tokenization
*
* @var array
* @access private
*/
var $tokenization;
/**
* Default Constructor.
*
* @return \phpseclib\File\ANSI
* @access public
*/
function __construct()
{
$attr_cell = new \stdClass();
$attr_cell->bold = false;
$attr_cell->underline = false;
$attr_cell->blink = false;
$attr_cell->background = 'black';
$attr_cell->foreground = 'white';
$attr_cell->reverse = false;
$this->base_attr_cell = clone $attr_cell;
$this->attr_cell = clone $attr_cell;
$this->setHistory(200);
$this->setDimensions(80, 24);
}
/**
* Set terminal width and height
*
* Resets the screen as well
*
* @param int $x
* @param int $y
* @access public
*/
function setDimensions($x, $y)
{
$this->max_x = $x - 1;
$this->max_y = $y - 1;
$this->x = $this->y = 0;
$this->history = $this->history_attrs = array();
$this->attr_row = array_fill(0, $this->max_x + 2, $this->base_attr_cell);
$this->screen = array_fill(0, $this->max_y + 1, '');
$this->attrs = array_fill(0, $this->max_y + 1, $this->attr_row);
$this->ansi = '';
}
/**
* Set the number of lines that should be logged past the terminal height
*
* @param int $history
* @access public
*/
function setHistory($history)
{
$this->max_history = $history;
}
/**
* Load a string
*
* @param string $source
* @access public
*/
function loadString($source)
{
$this->setDimensions($this->max_x + 1, $this->max_y + 1);
$this->appendString($source);
}
/**
* Appdend a string
*
* @param string $source
* @access public
*/
function appendString($source)
{
$this->tokenization = array('');
for ($i = 0; $i < strlen($source); $i++) {
if (strlen($this->ansi)) {
$this->ansi.= $source[$i];
$chr = ord($source[$i]);
// http://en.wikipedia.org/wiki/ANSI_escape_code#Sequence_elements
// single character CSI's not currently supported
switch (true) {
case $this->ansi == "\x1B=":
$this->ansi = '';
continue 2;
case strlen($this->ansi) == 2 && $chr >= 64 && $chr <= 95 && $chr != ord('['):
case strlen($this->ansi) > 2 && $chr >= 64 && $chr <= 126:
break;
default:
continue 2;
}
$this->tokenization[] = $this->ansi;
$this->tokenization[] = '';
// http://ascii-table.com/ansi-escape-sequences-vt-100.php
switch ($this->ansi) {
case "\x1B[H": // Move cursor to upper left corner
$this->old_x = $this->x;
$this->old_y = $this->y;
$this->x = $this->y = 0;
break;
case "\x1B[J": // Clear screen from cursor down
$this->history = array_merge($this->history, array_slice(array_splice($this->screen, $this->y + 1), 0, $this->old_y));
$this->screen = array_merge($this->screen, array_fill($this->y, $this->max_y, ''));
$this->history_attrs = array_merge($this->history_attrs, array_slice(array_splice($this->attrs, $this->y + 1), 0, $this->old_y));
$this->attrs = array_merge($this->attrs, array_fill($this->y, $this->max_y, $this->attr_row));
if (count($this->history) == $this->max_history) {
array_shift($this->history);
array_shift($this->history_attrs);
}
case "\x1B[K": // Clear screen from cursor right
$this->screen[$this->y] = substr($this->screen[$this->y], 0, $this->x);
array_splice($this->attrs[$this->y], $this->x + 1, $this->max_x - $this->x, array_fill($this->x, $this->max_x - ($this->x - 1), $this->base_attr_cell));
break;
case "\x1B[2K": // Clear entire line
$this->screen[$this->y] = str_repeat(' ', $this->x);
$this->attrs[$this->y] = $this->attr_row;
break;
case "\x1B[?1h": // set cursor key to application
case "\x1B[?25h": // show the cursor
case "\x1B(B": // set united states g0 character set
break;
case "\x1BE": // Move to next line
$this->_newLine();
$this->x = 0;
break;
default:
switch (true) {
case preg_match('#\x1B\[(\d+)B#', $this->ansi, $match): // Move cursor down n lines
$this->old_y = $this->y;
$this->y+= $match[1];
break;
case preg_match('#\x1B\[(\d+);(\d+)H#', $this->ansi, $match): // Move cursor to screen location v,h
$this->old_x = $this->x;
$this->old_y = $this->y;
$this->x = $match[2] - 1;
$this->y = $match[1] - 1;
break;
case preg_match('#\x1B\[(\d+)C#', $this->ansi, $match): // Move cursor right n lines
$this->old_x = $this->x;
$this->x+= $match[1];
break;
case preg_match('#\x1B\[(\d+)D#', $this->ansi, $match): // Move cursor left n lines
$this->old_x = $this->x;
$this->x-= $match[1];
if ($this->x < 0) {
$this->x = 0;
}
break;
case preg_match('#\x1B\[(\d+);(\d+)r#', $this->ansi, $match): // Set top and bottom lines of a window
break;
case preg_match('#\x1B\[(\d*(?:;\d*)*)m#', $this->ansi, $match): // character attributes
$attr_cell = &$this->attr_cell;
$mods = explode(';', $match[1]);
foreach ($mods as $mod) {
switch ($mod) {
case '':
case '0': // Turn off character attributes
$attr_cell = clone $this->base_attr_cell;
break;
case '1': // Turn bold mode on
$attr_cell->bold = true;
break;
case '4': // Turn underline mode on
$attr_cell->underline = true;
break;
case '5': // Turn blinking mode on
$attr_cell->blink = true;
break;
case '7': // Turn reverse video on
$attr_cell->reverse = !$attr_cell->reverse;
$temp = $attr_cell->background;
$attr_cell->background = $attr_cell->foreground;
$attr_cell->foreground = $temp;
break;
default: // set colors
//$front = $attr_cell->reverse ? &$attr_cell->background : &$attr_cell->foreground;
$front = &$attr_cell->{ $attr_cell->reverse ? 'background' : 'foreground' };
//$back = $attr_cell->reverse ? &$attr_cell->foreground : &$attr_cell->background;
$back = &$attr_cell->{ $attr_cell->reverse ? 'foreground' : 'background' };
switch ($mod) {
// @codingStandardsIgnoreStart
case '30': $front = 'black'; break;
case '31': $front = 'red'; break;
case '32': $front = 'green'; break;
case '33': $front = 'yellow'; break;
case '34': $front = 'blue'; break;
case '35': $front = 'magenta'; break;
case '36': $front = 'cyan'; break;
case '37': $front = 'white'; break;
case '40': $back = 'black'; break;
case '41': $back = 'red'; break;
case '42': $back = 'green'; break;
case '43': $back = 'yellow'; break;
case '44': $back = 'blue'; break;
case '45': $back = 'magenta'; break;
case '46': $back = 'cyan'; break;
case '47': $back = 'white'; break;
// @codingStandardsIgnoreEnd
default:
//user_error('Unsupported attribute: ' . $mod);
$this->ansi = '';
break 2;
}
}
}
break;
default:
//user_error("{$this->ansi} is unsupported\r\n");
}
}
$this->ansi = '';
continue;
}
$this->tokenization[count($this->tokenization) - 1].= $source[$i];
switch ($source[$i]) {
case "\r":
$this->x = 0;
break;
case "\n":
$this->_newLine();
break;
case "\x08": // backspace
if ($this->x) {
$this->x--;
$this->attrs[$this->y][$this->x] = clone $this->base_attr_cell;
$this->screen[$this->y] = substr_replace(
$this->screen[$this->y],
$source[$i],
$this->x,
1
);
}
break;
case "\x0F": // shift
break;
case "\x1B": // start ANSI escape code
$this->tokenization[count($this->tokenization) - 1] = substr($this->tokenization[count($this->tokenization) - 1], 0, -1);
//if (!strlen($this->tokenization[count($this->tokenization) - 1])) {
// array_pop($this->tokenization);
//}
$this->ansi.= "\x1B";
break;
default:
$this->attrs[$this->y][$this->x] = clone $this->attr_cell;
if ($this->x > strlen($this->screen[$this->y])) {
$this->screen[$this->y] = str_repeat(' ', $this->x);
}
$this->screen[$this->y] = substr_replace(
$this->screen[$this->y],
$source[$i],
$this->x,
1
);
if ($this->x > $this->max_x) {
$this->x = 0;
$this->_newLine();
} else {
$this->x++;
}
}
}
}
/**
* Add a new line
*
* Also update the $this->screen and $this->history buffers
*
* @access private
*/
function _newLine()
{
//if ($this->y < $this->max_y) {
// $this->y++;
//}
while ($this->y >= $this->max_y) {
$this->history = array_merge($this->history, array(array_shift($this->screen)));
$this->screen[] = '';
$this->history_attrs = array_merge($this->history_attrs, array(array_shift($this->attrs)));
$this->attrs[] = $this->attr_row;
if (count($this->history) >= $this->max_history) {
array_shift($this->history);
array_shift($this->history_attrs);
}
$this->y--;
}
$this->y++;
}
/**
* Returns the current coordinate without preformating
*
* @access private
* @return string
*/
function _processCoordinate($last_attr, $cur_attr, $char)
{
$output = '';
if ($last_attr != $cur_attr) {
$close = $open = '';
if ($last_attr->foreground != $cur_attr->foreground) {
if ($cur_attr->foreground != 'white') {
$open.= '<span style="color: ' . $cur_attr->foreground . '">';
}
if ($last_attr->foreground != 'white') {
$close = '</span>' . $close;
}
}
if ($last_attr->background != $cur_attr->background) {
if ($cur_attr->background != 'black') {
$open.= '<span style="background: ' . $cur_attr->background . '">';
}
if ($last_attr->background != 'black') {
$close = '</span>' . $close;
}
}
if ($last_attr->bold != $cur_attr->bold) {
if ($cur_attr->bold) {
$open.= '<b>';
} else {
$close = '</b>' . $close;
}
}
if ($last_attr->underline != $cur_attr->underline) {
if ($cur_attr->underline) {
$open.= '<u>';
} else {
$close = '</u>' . $close;
}
}
if ($last_attr->blink != $cur_attr->blink) {
if ($cur_attr->blink) {
$open.= '<blink>';
} else {
$close = '</blink>' . $close;
}
}
$output.= $close . $open;
}
$output.= htmlspecialchars($char);
return $output;
}
/**
* Returns the current screen without preformating
*
* @access private
* @return string
*/
function _getScreen()
{
$output = '';
$last_attr = $this->base_attr_cell;
for ($i = 0; $i <= $this->max_y; $i++) {
for ($j = 0; $j <= $this->max_x; $j++) {
$cur_attr = $this->attrs[$i][$j];
$output.= $this->_processCoordinate($last_attr, $cur_attr, isset($this->screen[$i][$j]) ? $this->screen[$i][$j] : '');
$last_attr = $this->attrs[$i][$j];
}
$output.= "\r\n";
}
$output = substr($output, 0, -2);
// close any remaining open tags
$output.= $this->_processCoordinate($last_attr, $this->base_attr_cell, '');
return rtrim($output);
}
/**
* Returns the current screen
*
* @access public
* @return string
*/
function getScreen()
{
return '<pre width="' . ($this->max_x + 1) . '" style="color: white; background: black">' . $this->_getScreen() . '</pre>';
}
/**
* Returns the current screen and the x previous lines
*
* @access public
* @return string
*/
function getHistory()
{
$scrollback = '';
$last_attr = $this->base_attr_cell;
for ($i = 0; $i < count($this->history); $i++) {
for ($j = 0; $j <= $this->max_x + 1; $j++) {
$cur_attr = $this->history_attrs[$i][$j];
$scrollback.= $this->_processCoordinate($last_attr, $cur_attr, isset($this->history[$i][$j]) ? $this->history[$i][$j] : '');
$last_attr = $this->history_attrs[$i][$j];
}
$scrollback.= "\r\n";
}
$base_attr_cell = $this->base_attr_cell;
$this->base_attr_cell = $last_attr;
$scrollback.= $this->_getScreen();
$this->base_attr_cell = $base_attr_cell;
return '<pre width="' . ($this->max_x + 1) . '" style="color: white; background: black">' . $scrollback . '</span></pre>';
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,47 @@
<?php
/**
* Pure-PHP ASN.1 Parser
*
* PHP version 5
*
* @category File
* @package ASN1
* @author Jim Wigginton <terrafrost@php.net>
* @copyright 2012 Jim Wigginton
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @link http://phpseclib.sourceforge.net
*/
namespace phpseclib\File\ASN1;
/**
* ASN.1 Element
*
* Bypass normal encoding rules in phpseclib\File\ASN1::encodeDER()
*
* @package ASN1
* @author Jim Wigginton <terrafrost@php.net>
* @access public
*/
class Element
{
/**
* Raw element value
*
* @var string
* @access private
*/
var $element;
/**
* Constructor
*
* @param string $encoded
* @return \phpseclib\File\ASN1\Element
* @access public
*/
function __construct($encoded)
{
$this->element = $encoded;
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,6 @@
# minimalist openssl.cnf file for use with phpseclib
HOME = .
RANDFILE = $ENV::HOME/.rnd
[ v3_ca ]

View file

@ -1,5 +1,4 @@
<?php
define('ROOT_DIR', dirname(__DIR__));
define('PLUGINS_DEST_DIR', __DIR__ . '/dist/releases/plugins');
is_dir(PLUGINS_DEST_DIR) || mkdir(PLUGINS_DEST_DIR, 0777, true);
@ -29,10 +28,46 @@ $keys = [
'url',
'version'
];
/*
$released = [
'add-x-originating-ip-header',
'avatars',
'backup',
'black-list',
'change-password',
'change-password-froxlor',
'change-password-hestia',
'change-password-hmailserver',
'change-password-ispconfig',
'change-password-poppassd',
'custom-login-mapping',
'imap-contacts-suggestions',
'kolab',
'ldap-contacts-suggestions',
'ldap-identities',
'ldap-login-mapping',
'ldap-mail-accounts',
'login-external',
'login-external-sso',
'login-override',
'login-register',
'login-remote',
'mailbox-detect',
'nextcloud',
'override-smtp-credentials',
'set-remote-addr',
'smtp-use-from-adr-account',
'snowfall-on-login-screen',
'two-factor-auth',
'view-ics',
'white-list'
];
*/
foreach (glob(ROOT_DIR . '/plugins/*', GLOB_NOSORT | GLOB_ONLYDIR) as $dir) {
if (is_file("{$dir}/index.php") && !strpos($dir, '.bak')) {
require "{$dir}/index.php";
$name = basename($dir);
// if (!in_array($name, $released)) continue;
$class = new ReflectionClass(str_replace('-', '', $name) . 'Plugin');
$manifest_item = [];
foreach ($class->getConstants() as $key => $value) {
@ -71,6 +106,7 @@ foreach (glob(ROOT_DIR . '/plugins/*', GLOB_NOSORT | GLOB_ONLYDIR) as $dir) {
$tar->compress(Phar::GZ);
unlink($tar_destination);
rename("{$tar_destination}.gz", $tgz_destination);
/*
if (Phar::canWrite()) {
$phar_destination = PLUGINS_DEST_DIR . "/{$name}.phar";
@unlink($phar_destination);
@ -80,6 +116,7 @@ foreach (glob(ROOT_DIR . '/plugins/*', GLOB_NOSORT | GLOB_ONLYDIR) as $dir) {
unlink($phar_destination);
rename("{$phar_destination}.gz", $phar_destination);
}
*/
if (isset($options['sign'])) {
passthru('gpg --local-user 1016E47079145542F8BA133548208BA13290F3EB --armor --detach-sign '.escapeshellarg($tgz_destination), $return_var);
$manifest_item['pgp_sig'] = trim(preg_replace('/-----(BEGIN|END) PGP SIGNATURE-----/', '', file_get_contents($tgz_destination.'.asc')));

280
cli/release.php Executable file
View file

@ -0,0 +1,280 @@
#!/usr/bin/php
<?php
define('ROOT_DIR', dirname(__DIR__));
chdir(ROOT_DIR);
$options = getopt('', ['aur','docker','plugins','skip-gulp','debian','nextcloud','owncloud','cpanel','sign']);
if (isset($options['plugins'])) {
require(ROOT_DIR . '/build/plugins.php');
}
$gulp = trim(shell_exec('which gulp'));
if (!$gulp) {
exit('gulp not installed, run as root: npm install --global gulp-cli');
}
$package = json_decode(file_get_contents('package.json'));
/**
* Update files that contain version
*/
// cloudron
$file = ROOT_DIR . '/integrations/cloudron/Dockerfile';
file_put_contents($file, preg_replace('/VERSION=[0-9.]+/', "VERSION={$package->version}", file_get_contents($file)));
$file = ROOT_DIR . '/integrations/cloudron/DESCRIPTION.md';
file_put_contents($file, preg_replace('/<upstream>[^<]*</', "<upstream>{$package->version}<", file_get_contents($file)));
// virtualmin
$file = ROOT_DIR . '/integrations/virtualmin/snappymail.pl';
file_put_contents($file, preg_replace('/return \\( "[0-9]+\\.[0-9]+\\.[0-9]+" \\)/', "return ( \"{$package->version}\" )", file_get_contents($file)));
// Arch User Repository
// https://aur.archlinux.org/packages/snappymail/
$options['aur'] = isset($options['aur']);
// Docker build
$options['docker'] = isset($options['docker']);
if ($options['docker'] && !($docker = trim(shell_exec('which docker')))) {
exit('docker not found');
}
if ($options['docker'] && $options['aur']) {
exit('Conflict between docker and aur');
}
$destPath = "build/dist/releases/webmail/{$package->version}/";
is_dir($destPath) || mkdir($destPath, 0777, true);
$zip_destination = "{$destPath}snappymail-{$package->version}.zip";
$tar_destination = "{$destPath}snappymail-{$package->version}.tar";
@unlink($zip_destination);
@unlink($tar_destination);
@unlink("{$tar_destination}.gz");
if (!isset($options['skip-gulp'])) {
echo "\x1b[33;1m === Gulp === \x1b[0m\n";
passthru($gulp, $return_var);
if ($return_var) {
exit("gulp failed with error code {$return_var}\n");
}
$cmddir = escapeshellcmd(ROOT_DIR) . '/snappymail/v/0.0.0/static';
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");
passthru("{$gzip} -k --best {$cmddir}/css/admin*.css");
passthru("{$gzip} -k --best {$cmddir}/css/app*.css");
unlink(ROOT_DIR . '/snappymail/v/0.0.0/static/js/boot.js.gz');
unlink(ROOT_DIR . '/snappymail/v/0.0.0/static/js/min/boot.min.js.gz');
}
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");
passthru("{$brotli} -k --best {$cmddir}/css/admin*.css");
passthru("{$brotli} -k --best {$cmddir}/css/app*.css");
unlink(ROOT_DIR . '/snappymail/v/0.0.0/static/js/boot.js.br');
unlink(ROOT_DIR . '/snappymail/v/0.0.0/static/js/min/boot.min.js.br');
}
}
// Temporary rename folder to speed up PharData
//if (!rename('snappymail/v/0.0.0', "snappymail/v/{$package->version}")){
if (!rename('snappymail/v/0.0.0', "snappymail/v/{$package->version}")) {
exit('Failed to temporary rename snappymail/v/0.0.0');
}
register_shutdown_function(function(){
// Rename folder back to original
@rename("snappymail/v/{$GLOBALS['package']->version}", 'snappymail/v/0.0.0');
});
echo "\x1b[33;1m === Zip/Tar === \x1b[0m\n";
$zip = new ZipArchive();
if (!$zip->open($zip_destination, ZIPARCHIVE::CREATE)) {
exit("Failed to create {$zip_destination}");
}
$tar = new PharData($tar_destination);
$files = new RecursiveIteratorIterator(new RecursiveDirectoryIterator('snappymail/v'), RecursiveIteratorIterator::SELF_FIRST);
foreach ($files as $file) {
$file = str_replace('\\', '/', $file);
//echo "{$file}\n";
// Ignore "." and ".." folders
if (!in_array(substr($file, strrpos($file, '/')+1), array('.', '..'))) {
if (is_dir($file)) {
$zip->addEmptyDir($file);
} else if (is_file($file)) {
$zip->addFile($file);
}
}
}
if ($options['docker']) {
$tar->buildFromDirectory('./snappymail/', "@v/{$package->version}@");
} else {
$tar->buildFromDirectory('./', "@snappymail/v/{$package->version}@");
}
//$zip->addFile('cli/upgrade.sh');
//$tar->addFile('cli/upgrade.sh');
$zip->addFile('data/.htaccess');
$tar->addFile('data/.htaccess');
$zip->addFromString('data/VERSION', $package->version);
$tar->addFromString('data/VERSION', $package->version);
$zip->addFile('data/README.md');
$tar->addFile('data/README.md');
if ($options['aur']) {
$data = '<?php
function __get_custom_data_full_path()
{
return \'/var/lib/snappymail\';
}
';
$zip->addFromString('include.php', $data);
$tar->addFromString('include.php', $data);
} else {
$zip->addFile('_include.php');
$tar->addFile('_include.php');
}
$zip->addFile('.htaccess');
$tar->addFile('.htaccess');
$index = file_get_contents('index.php');
$index = str_replace('0.0.0', $package->version, $index);
$zip->addFromString('index.php', $index);
$tar->addFromString('index.php', $index);
$zip->addFile('README.md');
$tar->addFile('README.md');
$zip->close();
$tar->compress(Phar::GZ);
unlink($tar_destination);
$tar_destination .= '.gz';
echo "{$zip_destination} created\n{$tar_destination} created\n";
if (isset($options['nextcloud'])) {
require(ROOT_DIR . '/build/nextcloud.php');
}
if (isset($options['owncloud'])) {
require(ROOT_DIR . '/build/owncloud.php');
}
if (isset($options['cpanel'])) {
require(ROOT_DIR . '/build/cpanel.php');
}
rename("snappymail/v/{$package->version}", 'snappymail/v/0.0.0');
file_put_contents("{$destPath}core.json", '{
"version": "'.$package->version.'",
"file": "../latest.tar.gz",
"warnings": []
}');
// Arch User Repository
if ($options['aur']) {
// extension_loaded('blake2')
if (!function_exists('b2sum') && $b2sum = trim(shell_exec('which b2sum'))) {
function b2sum($file) {
$file = escapeshellarg($file);
exec("b2sum --binary {$file} 2>&1", $output, $exitcode);
$output = explode(' ', implode("\n", $output));
return $output[0];
}
}
$b2sums = function_exists('b2sum') ? [
b2sum($tar_destination),
b2sum(ROOT_DIR . '/build/arch/snappymail.sysusers'),
b2sum(ROOT_DIR . '/build/arch/snappymail.tmpfiles')
] : [];
file_put_contents('build/arch/.SRCINFO', 'pkgbase = snappymail
pkgdesc = modern PHP webmail client
pkgver = '.$package->version.'
pkgrel = 1
url = https://github.com/the-djmaze/snappymail
arch = any
license = AGPL3
makedepends = php
makedepends = nodejs
makedepends = yarn
makedepends = gulp
depends = php-fpm
optdepends = mariadb: storage backend for contacts
optdepends = php-pgsql: storage backend for contacts
optdepends = php-sqlite: storage backend for contacts
source = snappymail-'.$package->version.'.tar.gz::https://github.com/the-djmaze/snappymail/archive/v'.$package->version.'.tar.gz
source = snappymail.sysusers
source = snappymail.tmpfiles
b2sums = '.implode("\n b2sums = ", $b2sums).'
pkgname = snappymail
');
$file = ROOT_DIR . '/build/arch/PKGBUILD';
if (is_file($file)) {
$PKGBUILD = file_get_contents($file);
$PKGBUILD = preg_replace('/pkgver=[0-9.]+/', "pkgver={$package->version}", $PKGBUILD);
$PKGBUILD = preg_replace('/b2sums=\\([^)]+\\)/s', "b2sums=('".implode("'\n '", $b2sums)."')", $PKGBUILD);
file_put_contents($file, $PKGBUILD);
}
}
// Debian Repository
else if (isset($options['debian'])) {
require(ROOT_DIR . '/build/deb.php');
}
// Docker build
else if ($options['docker']) {
echo "\x1b[33;1m === Docker === \x1b[0m\n";
$zip_filename = "snappymail-{$package->version}.zip";
copy($zip_destination, "./.docker/release/{$zip_filename}");
if ($docker) {
passthru("{$docker} build --pull " . ROOT_DIR . "/.docker/release/ --build-arg FILES_ZIP={$zip_filename} -t snappymail:{$package->version}");
} else {
echo "Docker not installed!\n";
}
}
if (isset($options['sign'])) {
echo "\x1b[33;1m === PGP Sign === \x1b[0m\n";
passthru('gpg --local-user 1016E47079145542F8BA133548208BA13290F3EB --armor --detach-sign '.escapeshellarg($tar_destination), $return_var);
passthru('gpg --local-user 1016E47079145542F8BA133548208BA13290F3EB --armor --detach-sign '.escapeshellarg($zip_destination), $return_var);
if (isset($options['nextcloud'])) {
passthru('gpg --local-user 1016E47079145542F8BA133548208BA13290F3EB --armor --detach-sign '
.escapeshellarg("{$destPath}snappymail-{$package->version}-nextcloud.tar.gz"), $return_var);
}
if (isset($options['owncloud'])) {
passthru('gpg --local-user 1016E47079145542F8BA133548208BA13290F3EB --armor --detach-sign '
.escapeshellarg("{$destPath}snappymail-{$package->version}-owncloud.tar.gz"), $return_var);
}
if (isset($options['cpanel'])) {
passthru('gpg --local-user 1016E47079145542F8BA133548208BA13290F3EB --armor --detach-sign '
.escapeshellarg("{$destPath}snappymail-{$package->version}-cpanel.tar.gz"), $return_var);
}
if (isset($options['debian'])) {
passthru('gpg --local-user 1016E47079145542F8BA133548208BA13290F3EB --armor --detach-sign '
. escapeshellarg(ROOT_DIR . "/build/dist/releases/webmail/{$package->version}/" . basename(DEB_DEST_DIR.'.deb')), $return_var);
// https://github.com/the-djmaze/snappymail/issues/185#issuecomment-1059420588
passthru('gpg --local-user 1016E47079145542F8BA133548208BA13290F3EB --digest-algo SHA512 --clearsign --output '
. escapeshellarg(ROOT_DIR . "/build/dist/releases/webmail/{$package->version}/InRelease") . ' '
. escapeshellarg(ROOT_DIR . "/build/dist/releases/webmail/{$package->version}/Release"), $return_var);
passthru('gpg --local-user 1016E47079145542F8BA133548208BA13290F3EB --digest-algo SHA512 -abs --output '
. escapeshellarg(ROOT_DIR . "/build/dist/releases/webmail/{$package->version}/Release.gpg") . ' '
. escapeshellarg(ROOT_DIR . "/build/dist/releases/webmail/{$package->version}/Release"), $return_var);
}
}

View file

@ -3,8 +3,10 @@ import ko from 'ko';
import { logoutLink } from 'Common/Links';
import { i18nToNodes, initOnStartOrLangChange } from 'Common/Translator';
import { arePopupsVisible } from 'Knoin/Knoin';
import { LanguageStore } from 'Stores/Language';
import { ThemeStore } from 'Stores/Theme';
import { initThemes } from 'Stores/Theme';
import { SelectComponent } from 'Component/Select';
import { CheckboxComponent } from 'Component/Checkbox';
@ -18,6 +20,7 @@ export class AbstractApp {
}
logoutReload(url) {
arePopupsVisible(false);
url = url || logoutLink();
if (location.href !== url) {
setTimeout(() => location.href = url, 100);
@ -46,7 +49,7 @@ export class AbstractApp {
initOnStartOrLangChange();
LanguageStore.populate();
ThemeStore.populate();
initThemes();
this.start();
}

View file

@ -1,7 +1,7 @@
import 'External/ko';
import { Settings, SettingsGet } from 'Common/Globals';
import { ThemeStore } from 'Stores/Theme';
import { SettingsGet, SettingsAdmin } from 'Common/Globals';
import { initThemes } from 'Stores/Theme';
import Remote from 'Remote/Admin/Fetch';
@ -11,6 +11,8 @@ import { LoginAdminScreen } from 'Screen/Admin/Login';
import { startScreens } from 'Knoin/Knoin';
import { AbstractApp } from 'App/Abstract';
import { AskPopupView } from 'View/Popup/Ask';
export class AdminApp extends AbstractApp {
constructor() {
super(Remote);
@ -18,19 +20,33 @@ export class AdminApp extends AbstractApp {
}
refresh() {
ThemeStore.populate();
initThemes();
this.start();
}
start() {
if (!Settings.app('adminAllowed')) {
// if (!Settings.app('adminAllowed')) {
if (!SettingsAdmin('allowed')) {
rl.route.root();
setTimeout(() => location.href = '/', 1);
} else if (SettingsGet('Auth')) {
this.weakPassword(SettingsGet('WeakPassword'));
this.weakPassword(SettingsGet('weakPassword'));
startScreens([SettingsAdminScreen]);
} else {
startScreens([LoginAdminScreen]);
}
}
}
AskPopupView.credentials = function(sAskDesc, btnText) {
return new Promise(resolve => {
this.showModal([
sAskDesc,
view => resolve({username:view.username(), password:view.passphrase()}),
() => resolve(null),
true,
3,
btnText
]);
});
};

View file

@ -1,8 +1,8 @@
import 'External/User/ko';
import { SMAudio } from 'Common/Audio';
import { isArray, pString } from 'Common/Utils';
import { mailToHelper, setLayoutResizer, dropdownsDetectVisibility } from 'Common/UtilsUser';
import { arrayLength } from 'Common/Utils';
import { mailToHelper, setLayoutResizer, dropdownsDetectVisibility, loadAccountsAndIdentities } from 'Common/UtilsUser';
import {
FolderType,
@ -27,25 +27,22 @@ 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';
import { AccountUserStore } from 'Stores/User/Account';
import { ContactUserStore } from 'Stores/User/Contact';
import { IdentityUserStore } from 'Stores/User/Identity';
import { FolderUserStore } from 'Stores/User/Folder';
import { PgpUserStore } from 'Stores/User/Pgp';
import { SMimeUserStore } from 'Stores/User/SMime';
import { MessagelistUserStore } from 'Stores/User/Messagelist';
import { ThemeStore, changeTheme } from 'Stores/Theme';
import { ThemeStore, initThemes } from 'Stores/Theme';
import { LanguageStore } from 'Stores/Language';
import { MessageUserStore } from 'Stores/User/Message';
import Remote from 'Remote/User/Fetch';
import { AccountModel } from 'Model/Account';
import { IdentityModel } from 'Model/Identity';
import { LoginUserScreen } from 'Screen/User/Login';
import { MailBoxUserScreen } from 'Screen/User/MailBox';
import { SettingsUserScreen } from 'Screen/User/Settings';
@ -61,9 +58,7 @@ import { AskPopupView } from 'View/Popup/Ask';
import {
folderInformation,
folderInformationMultiply,
refreshFoldersInterval,
messagesMoveHelper,
messagesDeleteHelper
setRefreshFoldersInterval
} from 'Common/Folders';
import { loadFolders } from 'Model/FolderCollection';
@ -79,7 +74,7 @@ export class AppUser extends AbstractApp {
(currentTime > (lastTime + interval + 1000))
&& Remote.request('Version',
iError => (100 < iError) && location.reload(),
{ Version: Settings.app('version') }
{ version: Settings.app('version') }
);
lastTime = currentTime;
}, interval);
@ -90,6 +85,11 @@ export class AppUser extends AbstractApp {
addEventListener('click', dropdownsDetectVisibility);
this.folderList = FolderUserStore.folderList;
this.messageList = MessagelistUserStore;
this.ask = AskPopupView;
this.loadAccountsAndIdentities = loadAccountsAndIdentities;
}
/**
@ -114,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;
@ -126,58 +126,20 @@ export class AppUser extends AbstractApp {
// no default
}
if (!oMoveFolder && !bDelete) {
showScreenPopup(FolderSystemPopupView, [nSetSystemFoldersNotification]);
} else if (bDelete) {
if (bDelete) {
showScreenPopup(AskPopupView, [
i18n('POPUPS_ASK/DESC_WANT_DELETE_MESSAGES'),
() => {
messagesDeleteHelper(sFromFolderFullName, oUids);
MessagelistUserStore.removeMessagesFromList(sFromFolderFullName, oUids);
MessagelistUserStore.moveMessages(sFromFolderFullName, oUids);
}
]);
} else if (oMoveFolder) {
messagesMoveHelper(sFromFolderFullName, oMoveFolder.fullName, oUids);
MessagelistUserStore.removeMessagesFromList(sFromFolderFullName, oUids, oMoveFolder.fullName);
MessagelistUserStore.moveMessages(sFromFolderFullName, oUids, oMoveFolder.fullName);
} else {
showScreenPopup(FolderSystemPopupView, [nSetSystemFoldersNotification]);
}
}
accountsAndIdentities() {
AccountUserStore.loading(true);
IdentityUserStore.loading(true);
Remote.request('AccountsAndIdentities', (iError, oData) => {
AccountUserStore.loading(false);
IdentityUserStore.loading(false);
if (!iError) {
let items = oData.Result.Accounts;
AccountUserStore(isArray(items)
? items.map(oValue => new AccountModel(oValue.email, oValue.name))
: []
);
AccountUserStore.unshift(new AccountModel(SettingsGet('MainEmail'), '', false));
items = oData.Result.Identities;
IdentityUserStore(isArray(items)
? items.map(identityData => {
const identity = new IdentityModel(
pString(identityData.Id),
pString(identityData.Email)
);
identity.name(pString(identityData.Name));
identity.replyTo(pString(identityData.ReplyTo));
identity.bcc(pString(identityData.Bcc));
identity.signature(pString(identityData.Signature));
identity.signatureInsertBefore(!!identityData.SignatureInsertBefore);
return identity;
})
: []
);
}
});
}
/**
* @param {string} folder
* @param {Array=} list = []
@ -187,25 +149,26 @@ export class AppUser extends AbstractApp {
}
logout() {
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() {
super.bootstart();
addEventListener('resize', () => leftPanelDisabled(ThemeStore.isMobile() || 1000 > innerWidth));
addEventListener('beforeunload', event => {
if (arePopupsVisible() || (ThemeStore.isMobile() && MessageUserStore.message())) {
if (arePopupsVisible() || (!SettingsUserStore.usePreviewPane() && MessageUserStore.message())) {
event.preventDefault();
return event.returnValue = "Are you sure you want to exit?";
return event.returnValue = i18n('POPUPS_ASK/EXIT_ARE_YOU_SURE');
}
}, {capture: true});
}
refresh() {
ThemeStore.populate();
LanguageStore.language(SettingsGet('Language'));
changeTheme(SettingsGet('Theme'));
initThemes();
LanguageStore.language(SettingsGet('language'));
this.start();
}
@ -221,25 +184,17 @@ export class AppUser extends AbstractApp {
SettingsUserStore.init();
ContactUserStore.init();
loadFolders(value => {
loadFolders((success, error) => {
try {
if (value) {
if (success) {
startScreens([
MailBoxUserScreen,
SettingsUserScreen
]);
setInterval(() => {
const cF = FolderUserStore.currentFolderFullName(),
iF = getFolderInboxName();
folderInformation(iF);
iF === cF || folderInformation(cF);
folderInformationMultiply();
}, refreshFoldersInterval);
setRefreshFoldersInterval(SettingsGet('CheckMailInterval'));
ContactUserStore.init();
this.accountsAndIdentities();
loadAccountsAndIdentities();
setTimeout(() => {
const cF = FolderUserStore.currentFolderFullName();
@ -256,38 +211,29 @@ export class AppUser extends AbstractApp {
// initLeftSideLayoutResizer
setTimeout(() => {
const left = elementById('rl-left'),
right = elementById('rl-right'),
fToggle = () =>
setLayoutResizer(left, right, ClientSideKeyNameFolderListSize,
setLayoutResizer(left, ClientSideKeyNameFolderListSize,
(ThemeStore.isMobile() || leftPanelDisabled()) ? 0 : 'Width');
if (left && right) {
if (left) {
fToggle();
leftPanelDisabled.subscribe(fToggle);
}
}, 1);
setInterval(reloadTime(), 60000);
setInterval(reloadTime, 60000);
PgpUserStore.init();
SMimeUserStore.loadCertificates();
// When auto-login is active
try {
navigator.registerProtocolHandler?.(
'mailto',
location.protocol + '//' + location.host + location.pathname + '?mailto&to=%s',
(SettingsGet('Title') || 'SnappyMail')
);
} catch (e) {} // eslint-disable-line no-empty
setTimeout(() => mailToHelper(SettingsGet('MailToEmail')), 500);
setTimeout(() => mailToHelper(SettingsGet('mailToEmail')), 500);
} else {
this.logout();
alert('Folders error: ' + getErrorMessage(0, error))
}
} catch (e) {
console.error(e);
}
});
} else {
startScreens([LoginUserScreen]);
}
@ -295,6 +241,62 @@ 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, ask) {
return new Promise(resolve => {
this.showModal([
sAskDesc,
view => resolve({
password:view.passphrase(),
username:/*ask & 2 ? */view.username(),
remember:/*ask & 4 ? */view.remember(),
rememberPermanent: view.rememberPermanent(),
}),
() => resolve(null),
true,
ask || 1,
btnText
]);
});
};
AskPopupView.cryptkey = () => new Promise(resolve => {
const fn = () => AskPopupView.showModal([
i18n('CRYPTO/ASK_CRYPTKEY_PASS'),
view => {
let pass = view.passphrase();
if (pass) {
Remote.post('ResealCryptKey', null, {
passphrase: pass
}).then(response => {
resolve(response?.Result);
}).catch(e => {
if (111 === e.code) {
fn();
} else {
console.error(e);
resolve(null);
}
});
} else {
resolve(null);
}
},
() => resolve(null),
true,
1,
i18n('CRYPTO/DECRYPT')
]);
fn();
});

View file

@ -116,6 +116,7 @@ export const SMAudio = new class {
if ('running' == audioCtx.state && (this.supportedMp3 || this.supportedOgg)) {
notificator = notificator || createNewObject();
if (notificator) {
// SettingsGet('NotificationSound').startsWith('custom@')
notificator.src = Links.staticLink('sounds/'
+ SettingsGet('NotificationSound')
+ (this.supportedMp3 ? '.mp3' : '.ogg'));

View file

@ -1,9 +1,5 @@
import { MessageSetAction } from 'Common/EnumsUser';
import { isArray } from 'Common/Utils';
let FOLDERS_CACHE = {},
FOLDERS_NAME_CACHE = {},
MESSAGE_FLAGS_CACHE = {},
let FOLDERS_CACHE = new Map,
FOLDERS_HASH_MAP = new Map,
inboxFolderName = 'INBOX';
export const
@ -11,18 +7,10 @@ export const
* @returns {void}
*/
clearCache = () => {
FOLDERS_CACHE = {};
FOLDERS_NAME_CACHE = {};
MESSAGE_FLAGS_CACHE = {};
FOLDERS_CACHE.clear();
FOLDERS_HASH_MAP.clear();
},
/**
* @param {string} folderFullName
* @param {string} uid
* @returns {string}
*/
getMessageKey = (folderFullName, uid) => folderFullName + '#' + uid,
/**
* @returns {string}
*/
@ -34,143 +22,35 @@ export const
setFolderInboxName = name => inboxFolderName = name,
/**
* @param {string} folderHash
* @param {string} fullNameHash
* @returns {string}
*/
getFolderFullName = folderHash => (folderHash && FOLDERS_NAME_CACHE[folderHash]) || '',
getFolderFromHashMap = fullNameHash => getFolderFromCacheList(FOLDERS_HASH_MAP.get(fullNameHash)),
/**
* @param {string} folderHash
* @param {string} folderFullName
* @param {?FolderModel} folder
*/
setFolder = folder => {
folder.hash = '';
FOLDERS_CACHE[folder.fullName] = folder;
FOLDERS_NAME_CACHE[folder.fullNameHash] = folder.fullName;
folder.etag = '';
FOLDERS_CACHE.set(folder.fullName, folder);
FOLDERS_HASH_MAP.set(folder.fullNameHash, folder.fullName);
},
/**
* @param {string} folderFullName
* @param {string} folderHash
* @param {string} folderETag
*/
setFolderHash = (folderFullName, folderHash) =>
FOLDERS_CACHE[folderFullName] && (FOLDERS_CACHE[folderFullName].hash = folderHash),
setFolderETag = (folderFullName, folderETag) =>
FOLDERS_CACHE.has(folderFullName) && (FOLDERS_CACHE.get(folderFullName).etag = folderETag),
/**
* @param {string} folderFullName
* @returns {?FolderModel}
*/
getFolderFromCacheList = folderFullName =>
FOLDERS_CACHE[folderFullName] ? FOLDERS_CACHE[folderFullName] : null,
FOLDERS_CACHE.get(folderFullName),
/**
* @param {string} folderFullName
*/
removeFolderFromCacheList = folderFullName => delete FOLDERS_CACHE[folderFullName];
export class MessageFlagsCache
{
/**
* @param {string} folderFullName
* @param {string} uid
* @param {string} flag
* @returns {bool}
*/
static hasFlag(folderFullName, uid, flag) {
return MESSAGE_FLAGS_CACHE[folderFullName]?.[uid]?.includes(flag);
}
/**
* @param {string} folderFullName
* @param {string} uid
* @returns {?Array}
*/
static getFor(folderFullName, uid) {
return MESSAGE_FLAGS_CACHE[folderFullName]?.[uid];
}
/**
* @param {string} folderFullName
* @param {string} uid
* @param {Array} flagsCache
*/
static setFor(folderFullName, uid, flags) {
if (isArray(flags)) {
if (!MESSAGE_FLAGS_CACHE[folderFullName]) {
MESSAGE_FLAGS_CACHE[folderFullName] = {};
}
MESSAGE_FLAGS_CACHE[folderFullName][uid] = flags;
}
}
/**
* @param {string} folderFullName
*/
static clearFolder(folderFullName) {
MESSAGE_FLAGS_CACHE[folderFullName] = {};
}
/**
* @param {(MessageModel|null)} message
*/
static initMessage(message) {
if (message) {
const uid = message.uid,
flags = this.getFor(message.folder, uid),
thread = message.threads();
isArray(flags) && message.flags(flags);
if (thread.length) {
message.hasUnseenSubMessage(!!thread.find(iSubUid =>
(uid !== iSubUid) && !this.hasFlag(message.folder, iSubUid, '\\seen')
));
message.hasFlaggedSubMessage(!!thread.find(iSubUid =>
(uid !== iSubUid) && this.hasFlag(message.folder, iSubUid, '\\flagged')
));
}
}
}
/**
* @param {(MessageModel|null)} message
*/
static store(message) {
message && this.setFor(message.folder, message.uid, message.flags());
}
/**
* @param {string} folder
* @param {string} uid
* @param {number} setAction
*/
static storeBySetAction(folder, uid, setAction) {
let flags = this.getFor(folder, uid) || [];
const
unread = flags.includes('\\seen') ? 0 : 1,
add = item => flags.includes(item) || flags.push(item),
remove = item => flags = flags.filter(flag => flag != item);
switch (setAction) {
case MessageSetAction.SetSeen:
add('\\seen');
break;
case MessageSetAction.UnsetSeen:
remove('\\seen');
break;
case MessageSetAction.SetFlag:
add('\\flagged');
break;
case MessageSetAction.UnsetFlag:
remove('\\flagged');
break;
// no default
}
this.setFor(folder, uid, flags);
return unread;
}
}
removeFolderFromCacheList = folderFullName => FOLDERS_CACHE.delete(folderFullName);

View file

@ -5,12 +5,10 @@ export const
/**
* @enum {string}
*/
Scope = {
MessageList: 'MessageList',
FolderList: 'FolderList',
MessageView: 'MessageView',
Settings: 'Settings'
},
ScopeMessageList = 'MessageList',
ScopeFolderList = 'FolderList',
ScopeMessageView = 'MessageView',
ScopeSettings = 'Settings',
/**
* @enum {number}
@ -39,7 +37,7 @@ SaveSettingStatus = {
/**
* @enum {number}
*/
Notification = {
Notifications = {
RequestError: 1,
RequestAborted: 2,
RequestTimeout: 3,
@ -52,6 +50,7 @@ Notification = {
ConnectionError: 104,
DomainNotAllowed: 109,
AccountNotAllowed: 110,
CryptKeyError: 111,
ContactsSyncError: 140,
@ -97,7 +96,6 @@ Notification = {
JsonParse: 952,
// JsonTimeout: 953,
UnknownNotification: 998,
UnknownError: 999,
// Admin

View file

@ -25,12 +25,12 @@ export const FolderType = {
FILES : 25;
JOURNAL : 26;
*/
};
},
/**
* @enum {string}
*/
export const FolderMetadataKeys = {
FolderMetadataKeys = {
// RFC 5464
Comment: '/private/comment',
CommentShared: '/shared/comment',
@ -39,12 +39,12 @@ export const FolderMetadataKeys = {
// Kolab
KolabFolderType: '/private/vendor/kolab/folder-type',
KolabFolderTypeShared: '/shared/vendor/kolab/folder-type'
};
},
/**
* @enum {string}
*/
export const ComposeType = {
ComposeType = {
Empty: 0,
Reply: 1,
ReplyAll: 2,
@ -52,51 +52,34 @@ export const ComposeType = {
ForwardAsAttachment: 4,
Draft: 5,
EditAsNew: 6
};
},
/**
* @enum {number}
*/
export const
ClientSideKeyNameExpandedFolders = 3,
ClientSideKeyNameFolderListSize = 4,
ClientSideKeyNameMessageListSize = 5,
ClientSideKeyNameLastSignMe = 7,
ClientSideKeyNameMessageHeaderFullInfo = 9,
ClientSideKeyNameMessageAttachmentControls = 10;
ClientSideKeyNameExpandedFolders = 3,
ClientSideKeyNameFolderListSize = 4,
ClientSideKeyNameMessageListSize = 5,
ClientSideKeyNameLastSignMe = 7,
ClientSideKeyNameMessageHeaderFullInfo = 9,
ClientSideKeyNameMessageAttachmentControls = 10,
/**
* @enum {number}
*/
export const MessageSetAction = {
MessageSetAction = {
SetSeen: 0,
UnsetSeen: 1,
SetFlag: 2,
UnsetFlag: 3
};
UnsetFlag: 3,
SetDeleted: 4,
UnsetDeleted: 5
},
/**
* @enum {number}
*/
export const MessagePriority = {
Low: 5,
Normal: 3,
High: 1
};
/**
* @enum {string}
*/
export const EditorDefaultType = {
Html: 'Html',
Plain: 'Plain'
};
/**
* @enum {number}
*/
export const Layout = {
NoPreview: 0,
SidePreview: 1,
BottomPreview: 2
};
//LayoutNoView = 0,
LayoutSideView = 1,
LayoutBottomView = 2
;

View file

@ -1,7 +1,9 @@
/* eslint key-spacing: 0 */
/* eslint quote-props: 0 */
import { arrayLength } from 'Common/Utils';
import { arrayLength, pInt } from 'Common/Utils';
export const RFC822 = 'message/rfc822';
const
cache = {},
@ -12,8 +14,8 @@ const
lowerCase = text => text.toLowerCase().trim(),
exts = {
eml: 'message/rfc822',
mime: 'message/rfc822',
eml: RFC822,
mime: RFC822,
vcard: 'text/vcard',
vcf: 'text/vcard',
htm: 'text/html',
@ -28,6 +30,8 @@ const
p7c: app+'pkcs7-mime',
p7m: app+'pkcs7-mime',
p7s: app+'pkcs7-signature',
p12: app+'pkcs12',
pfx: app+'x-pkcs12',
torrent: app+'x-bittorrent',
// scripts
@ -116,7 +120,8 @@ export const FileType = {
Spreadsheet: 'spreadsheet',
Presentation: 'presentation',
Certificate: 'certificate',
Archive: 'archive'
Archive: 'archive',
Calendar: 'calendar'
};
export const FileInfo = {
@ -133,7 +138,7 @@ export const FileInfo = {
getContentType: fileName => {
fileName = lowerCase(fileName);
if ('winmail.dat' === fileName) {
return app + 'ms-tnef';
return app + 'vnd.ms-tnef';
}
let ext = fileName.split('.').pop();
if (/^(txt|text|def|list|in|ini|log|sql|cfg|conf)$/.test(ext))
@ -161,7 +166,7 @@ export const FileInfo = {
*/
getType: (ext, mimeType) => {
ext = lowerCase(ext);
mimeType = lowerCase(mimeType).replace('csv/plain', 'text/csv');
mimeType = lowerCase(mimeType).replace('csv/plain', 'text/csv').replace('x-','');
let key = ext + mimeType;
if (cache[key]) {
@ -170,7 +175,7 @@ export const FileInfo = {
let result = FileType.Unknown;
const mimeTypeParts = mimeType.split('/'),
type = mimeTypeParts[1].replace('x-','').replace('-compressed',''),
type = mimeTypeParts[1].replace('-compressed',''),
match = str => mimeType.includes(str),
archive = /^(zip|7z|tar|rar|gzip|bzip|bzip2)$/;
@ -187,9 +192,12 @@ export const FileInfo = {
case ['php', 'js', 'css', 'xml', 'html'].includes(ext) || 'text/html' == mimeType:
result = FileType.Code;
break;
case 'eml' == ext || ['message/delivery-status', 'message/rfc822'].includes(mimeType):
case 'eml' == ext || ['message/delivery-status', RFC822].includes(mimeType):
result = FileType.Eml;
break;
case 'ics' == ext || mimeType == 'text/calendar':
result = FileType.Calendar;
break;
case 'text' == mimeTypeParts[0] || 'txt' == ext || 'log' == ext:
result = FileType.Text;
break;
@ -199,9 +207,8 @@ export const FileInfo = {
case 'pdf' == type || 'pdf' == ext:
result = FileType.Pdf;
break;
case [app+'pgp-signature', app+'pgp-keys'].includes(mimeType)
|| ['asc', 'pem', 'ppk'].includes(ext)
|| [app+'pkcs7-signature'].includes(mimeType) || 'p7s' == ext:
case [app+'pgp-signature', app+'pgp-keys', exts.p7m, exts.p7s, exts.p12, exts.pfx].includes(mimeType)
|| ['asc', 'pem', 'ppk', 'p7s', 'p7m', 'p12', 'pfx'].includes(ext):
result = FileType.Certificate;
break;
case match(msOffice+'.wordprocessingml') || match(openDoc+'.text') || match('vnd.ms-word')
@ -240,6 +247,7 @@ export const FileInfo = {
case FileType.Certificate:
case FileType.Spreadsheet:
case FileType.Presentation:
case FileType.Calendar:
return result + '-' + fileType;
}
return result;
@ -266,8 +274,8 @@ export const FileInfo = {
},
friendlySize: bytes => {
bytes = parseInt(bytes, 10) || 0;
let i = Math.floor(Math.log(bytes) / Math.log(1024));
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,24 +1,35 @@
import { isArray, arrayLength } from 'Common/Utils';
import {
MessageFlagsCache,
setFolderHash,
getFolderInboxName,
getFolderFromCacheList
} from 'Common/Cache';
import { RFC822 } from 'Common/File';
import { getFolderInboxName, getFolderFromCacheList } from 'Common/Cache';
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';
import { getNotification } from 'Common/Translator';
import { Settings, } from 'Common/Globals';
import { serverRequest } from 'Common/Links';
import Remote from 'Remote/User/Fetch';
let refreshInterval,
// Default every 15 minutes
refreshFoldersInterval = 900000;
export const
setRefreshFoldersInterval = minutes => {
refreshFoldersInterval = Math.max(1, pInt(SettingsGet('minRefreshInterval')), pInt(minutes)) * 60000;
clearInterval(refreshInterval);
refreshInterval = setInterval(() => {
const cF = FolderUserStore.currentFolderFullName(),
iF = getFolderInboxName();
folderInformation(iF);
iF === cF || folderInformation(cF);
folderInformationMultiply();
}, refreshFoldersInterval);
},
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))
);
@ -27,37 +38,6 @@ sortFolders = folders => {
}
},
/**
* @param {?Function} fCallback
* @param {string} folder
* @param {Array=} list = []
*/
fetchFolderInformation = (fCallback, folder, list = []) => {
let fetch = !arrayLength(list);
const uids = [],
folderFromCache = getFolderFromCacheList(folder);
if (!fetch) {
list.forEach(messageListItem => {
MessageFlagsCache.getFor(folder, messageListItem.uid) || uids.push(messageListItem.uid);
messageListItem.threads.forEach(uid => {
MessageFlagsCache.getFor(folder, uid) || uids.push(uid);
});
});
fetch = uids.length;
}
if (fetch) {
Remote.request('FolderInformation', fCallback, {
Folder: folder,
FlagsUids: uids,
UidNext: folderFromCache?.uidNext || 0 // Used to check for new messages
});
} else if (SettingsUserStore.useThreads()) {
MessagelistUserStore.reloadFlagsAndCachedMessage();
}
},
/**
* @param {Array=} aDisabled
* @param {Array=} aHeaderLines
@ -70,15 +50,14 @@ folderListOptionsBuilder = (
aDisabled,
aHeaderLines,
fRenameCallback,
fDisableCallback,
bNoSelectSelectable,
aList = FolderUserStore.folderList()
fDisableCallback
) => {
const
aResult = [],
sDeepPrefix = '\u00A0\u00A0\u00A0',
// FolderSystemPopupView should always be true
showUnsubscribed = fRenameCallback ? !SettingsUserStore.hideUnsubscribed() : true,
isDisabled = fDisableCallback || (item => !item.selectable() || aDisabled.includes(item.fullName)),
foldersWalk = folders => {
folders.forEach(oItem => {
@ -89,10 +68,7 @@ folderListOptionsBuilder = (
sDeepPrefix.repeat(oItem.deep) +
fRenameCallback(oItem),
system: false,
disabled: !bNoSelectSelectable && (
!oItem.selectable() ||
aDisabled.includes(oItem.fullName) ||
fDisableCallback(oItem))
disabled: isDisabled(oItem)
});
}
foldersWalk(oItem.subFolders());
@ -113,61 +89,64 @@ folderListOptionsBuilder = (
})
);
foldersWalk(aList);
foldersWalk(FolderUserStore.folderList());
return aResult;
},
// Every 5 minutes
refreshFoldersInterval = 300000,
/**
* @param {string} folder
* @param {Array=} list = []
*/
folderInformation = (folder, list) => {
if (folder?.trim()) {
fetchFolderInformation(
(iError, data) => {
let count = 1;
const uids = [];
if (arrayLength(list)) {
list.forEach(messageListItem => {
uids.push(messageListItem.uid);
messageListItem.threads.forEach(uid => uids.push(uid));
});
count = uids.length;
}
if (count) {
Remote.request('FolderInformation', (iError, data) => {
if (!iError && data.Result) {
const result = data.Result,
folderFromCache = getFolderFromCacheList(result.Folder);
folderFromCache = getFolderFromCacheList(result.name);
if (folderFromCache) {
const oldHash = folderFromCache.hash,
const oldHash = folderFromCache.etag,
unreadCountChange = (folderFromCache.unreadEmails() !== result.unreadEmails);
// folderFromCache.revivePropertiesFromJson(result);
// folderFromCache.revivePropertiesFromJson(result);
folderFromCache.expires = Date.now();
folderFromCache.uidNext = result.UidNext;
folderFromCache.hash = result.Hash;
folderFromCache.uidNext = result.uidNext;
folderFromCache.etag = result.etag;
folderFromCache.totalEmails(result.totalEmails);
folderFromCache.unreadEmails(result.unreadEmails);
unreadCountChange && MessageFlagsCache.clearFolder(folderFromCache.fullName);
MessagelistUserStore.notifyNewMessages(folderFromCache.fullName, result.newMessages);
if (result.MessagesFlags.length) {
result.MessagesFlags.forEach(message =>
MessageFlagsCache.setFor(folderFromCache.fullName, message.Uid.toString(), message.Flags)
);
MessagelistUserStore.reloadFlagsAndCachedMessage();
}
MessagelistUserStore.notifyNewMessages(folderFromCache.fullName, result.NewMessages);
if (!oldHash || unreadCountChange || result.Hash !== oldHash) {
if (!oldHash || unreadCountChange || result.etag !== oldHash) {
if (folderFromCache.fullName === FolderUserStore.currentFolderFullName()) {
MessagelistUserStore.reload();
/*
} else if (getFolderInboxName() === folderFromCache.fullName) {
Remote.messageList(null, {Folder: getFolderInboxName()}, true);
// Remote.messageList(null, {folder: getFolderFromCacheList(getFolderInboxName())}, true);
Remote.messageList(null, {folder: getFolderInboxName()}, true);
*/
}
}
}
}
},
folder,
list
);
}, {
folder: folder,
flagsUids: uids,
uidNext: getFolderFromCacheList(folder)?.uidNext || 0 // Used to check for new messages
});
}
}
},
@ -181,21 +160,18 @@ folderInformationMultiply = (boot = false) => {
if (!iError && arrayLength(oData.Result)) {
const utc = Date.now();
oData.Result.forEach(item => {
const folder = getFolderFromCacheList(item.Folder);
const folder = getFolderFromCacheList(item.name);
if (folder) {
const oldHash = folder.hash,
const oldHash = folder.etag,
unreadCountChange = folder.unreadEmails() !== item.unreadEmails;
// folder.revivePropertiesFromJson(item);
folder.expires = utc;
folder.hash = item.Hash;
folder.etag = item.etag;
folder.totalEmails(item.totalEmails);
folder.unreadEmails(item.unreadEmails);
unreadCountChange && MessageFlagsCache.clearFolder(folder.fullName);
if (!oldHash || item.Hash !== oldHash) {
if (!oldHash || item.etag !== oldHash) {
if (folder.fullName === FolderUserStore.currentFolderFullName()) {
MessagelistUserStore.reload();
}
@ -210,104 +186,26 @@ folderInformationMultiply = (boot = false) => {
boot && setTimeout(() => folderInformationMultiply(true), 2000);
}
}, {
Folders: folders
folders: folders
});
}
},
moveOrDeleteResponseHelper = (iError, oData) => {
if (iError) {
setFolderHash(FolderUserStore.currentFolderFullName(), '');
alert(getNotification(iError));
} else if (FolderUserStore.currentFolder()) {
if (2 === arrayLength(oData.Result)) {
setFolderHash(oData.Result[0], oData.Result[1]);
} else {
setFolderHash(FolderUserStore.currentFolderFullName(), '');
}
MessagelistUserStore.reload(!MessagelistUserStore.length);
}
},
messagesMoveHelper = (fromFolderFullName, toFolderFullName, uidsForMove) => {
const
sSpamFolder = FolderUserStore.spamFolder(),
isSpam = sSpamFolder === toFolderFullName,
isHam = !isSpam && sSpamFolder === fromFolderFullName && getFolderInboxName() === toFolderFullName;
Remote.abort('MessageList').request('MessageMove',
moveOrDeleteResponseHelper,
{
FromFolder: fromFolderFullName,
ToFolder: toFolderFullName,
Uids: [...uidsForMove].join(','),
MarkAsRead: (isSpam || FolderUserStore.trashFolder() === toFolderFullName) ? 1 : 0,
Learning: isSpam ? 'SPAM' : isHam ? 'HAM' : ''
}
);
},
messagesDeleteHelper = (sFromFolderFullName, aUidForRemove) => {
Remote.abort('MessageList').request('MessageDelete',
moveOrDeleteResponseHelper,
{
Folder: sFromFolderFullName,
Uids: [...aUidForRemove].join(',')
}
);
},
/**
* @param {string} sFromFolderFullName
* @param {Set} oUids
* @param {string} sToFolderFullName
* @param {boolean=} bCopy = false
*/
moveMessagesToFolder = (sFromFolderFullName, oUids, sToFolderFullName, bCopy) => {
if (sFromFolderFullName !== sToFolderFullName && oUids?.size) {
const oFromFolder = getFolderFromCacheList(sFromFolderFullName),
oToFolder = getFolderFromCacheList(sToFolderFullName);
if (oFromFolder && oToFolder) {
bCopy
? Remote.request('MessageCopy', null, {
FromFolder: oFromFolder.fullName,
ToFolder: oToFolder.fullName,
Uids: [...oUids].join(',')
})
: messagesMoveHelper(oFromFolder.fullName, oToFolder.fullName, oUids);
MessagelistUserStore.removeMessagesFromList(oFromFolder.fullName, oUids, oToFolder.fullName, bCopy);
return true;
}
}
return false;
},
dropFilesInFolder = (sFolderFullName, files) => {
let count = 0,
fn = () => 0 == --count
&& FolderUserStore.currentFolderFullName() == sFolderFullName
&& MessagelistUserStore.reload(true, true);
let count = files.length;
for (const file of files) {
if ('message/rfc822' === file.type) {
++count;
if (RFC822 === file.type) {
let data = new FormData;
data.append('Folder', sFolderFullName);
data.append('AppendFile', file);
data.XToken = Settings.app('token');
fetch(serverRequest('Append'), {
method: 'POST',
mode: 'same-origin',
cache: 'no-cache',
redirect: 'error',
referrerPolicy: 'no-referrer',
credentials: 'same-origin',
body: data
})
.then(fn)
.catch(fn);
data.append('folder', sFolderFullName);
data.append('appendFile', file);
Remote.request('FolderAppend', (iError, data)=>{
iError && console.error(data.message);
0 == --count
&& FolderUserStore.currentFolderFullName() == sFolderFullName
&& MessagelistUserStore.reload(true, true);
}, data);
} else {
--count;
}
}
};

View file

@ -1,20 +1,19 @@
import { doc, elementById, $htmlCL } from 'Common/Globals';
import { doc, appEl, $htmlCL } from 'Common/Globals';
// Fullscreen must be on app, else other popups fail
export const
app = elementById('rl-app'),
appFullscreen = () => (doc.fullscreenElement || doc.webkitFullscreenElement) === app,
appFullscreen = () => (doc.fullscreenElement || doc.webkitFullscreenElement) === appEl,
exitFullscreen = () => appFullscreen() && (doc.exitFullscreen || doc.webkitExitFullscreen).call(doc),
isFullscreen = ko.observable(false),
toggleFullscreen = () => isFullscreen() ? exitFullscreen() : app.requestFullscreen();
toggleFullscreen = () => isFullscreen() ? exitFullscreen() : appEl.requestFullscreen();
if (app) {
if (appEl) {
let event = 'fullscreenchange';
if (!app.requestFullscreen && app.webkitRequestFullscreen) {
app.requestFullscreen = app.webkitRequestFullscreen;
if (!appEl.requestFullscreen && appEl.webkitRequestFullscreen) {
appEl.requestFullscreen = appEl.webkitRequestFullscreen;
event = 'webkit'+event;
}
if (app.requestFullscreen) {
if (appEl.requestFullscreen) {
doc.addEventListener(event, () => {
isFullscreen(appFullscreen());
$htmlCL.toggle('rl-fullscreen', appFullscreen());

View file

@ -11,8 +11,11 @@ export const
elementById = id => doc.getElementById(id),
appEl = elementById('rl-app'),
Settings = rl.settings,
SettingsGet = Settings.get,
SettingsAdmin = name => (SettingsGet('Admin') || {})[name],
SettingsCapa = name => name && !!(SettingsGet('Capa') || {})[name],
dropdowns = [],
@ -31,6 +34,11 @@ export const
new CustomEvent(name, {detail:detail, cancelable: !!cancelable})
),
stopEvent = event => {
event.preventDefault();
event.stopPropagation();
},
formFieldFocused = () => doc.activeElement?.matches('input,textarea'),
addShortcut = (...args) => shortcuts.add(...args),

View file

@ -1,9 +1,12 @@
import { createElement } from 'Common/Globals';
import { forEachObjectEntry, pInt } from 'Common/Utils';
import { forEachObjectEntry, isArray, pInt } from 'Common/Utils';
import { SettingsUserStore } from 'Stores/User/Settings';
const
tpl = createElement('template'),
tmpl = createElement('template'),
turndown = new TurndownService(),
htmlre = /[&<>"']/g,
httpre = /^(https?:)?\/\//i,
htmlmap = {
@ -14,13 +17,52 @@ const
"'": '&#x27;'
},
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 => {
[...tmpl.content.querySelectorAll('blockquote')].reverse().forEach(node => {
const el = createElement('details', {class:'sm-bq-switcher'});
el.innerHTML = '<summary>•••</summary>';
node.replaceWith(el);
el.append(node);
});
},
replaceWithChildren = node => node.replaceWith(...[...node.childNodes]),
url = /(^|\s|\n|\/?>)(https?:\/\/[-A-Z0-9+&#/%?=()~_|!:,.;]*[-A-Z0-9+&#/%=~()_|])/gi,
urlRegExp = /https?:\/\/[^\p{C}\p{Z}]+[^\p{C}\p{Z}.]/gu,
// eslint-disable-next-line max-len
email = /(^|\s|\n|\/?>)((?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x21\x23-\x5b\x5d-\x7f]|\\[\x21\x23-\x5b\x5d-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x21-\x5a\x53-\x7f]|\\[\x21\x23-\x5b\x5d-\x7f])+)\]))/gi,
email = /(^|\r|\n|\p{C}\p{Z})((?:[^"(),.:;<>@[\]\\\p{C}\p{Z}]+(?:\.[^"(),.:;<>@[\]\\\p{C}\p{Z}]+)*|"(?:\\?[^"\\\p{C}\p{Z}])*")@[^@\p{C}\p{Z}]+[^@\p{C}\p{Z}.])/gui,
// rfc3966
tel = /(^|\s|\n|\/?>)(tel:(\+[0-9().-]+|[0-9*#().-]+(;phone-context=\+[0-9+().-]+)?))/g,
tel = /(tel:(\+[0-9().-]+|[0-9*#().-]+(;phone-context=\+[0-9+().-]+)?))/g,
// Strip tracking
/** TODO: implement other url strippers like from
@ -31,17 +73,21 @@ const
* https://github.com/M66B/FairEmail/blob/master/app/src/main/java/eu/faircode/email/UriHelper.java
*/
// eslint-disable-next-line max-len
stripParams = /^(utm_|ec_|fbclid|mc_eid|mkt_tok|_hsenc|vero_id|oly_enc_id|oly_anon_id|__s|Referrer|mailing|elq|bch|trc|ref|correlation_id|pd_|pf_)/i,
stripParams = /^(utm_|ec_|fbclid|mc_eid|mkt_tok|_hsenc|vero_id|oly_enc_id|oly_anon_id|__s|Referrer|mailing|elq|bch|trc|ref|correlation_id|pd_|pf_|email_hash)$/i,
urlGetParam = (url, name) => new URL(url).searchParams.get(name) || url,
base64Url = data => atob(data.replace(/_/g,'/').replace(/-/g,'+')),
decode = decodeURIComponent,
stripTracking = url => {
try {
let nurl = url
// Copernica
.replace(/^.+\/(https%253A[^/?&]+).*$/i, (...m) => decode(decode(m[1])))
.replace(/tracking\.(printabout\.nl[^?]+)\?.*/i, (...m) => m[1])
.replace(/(zalando\.nl[^?]+)\?.*/i, (...m) => m[1])
.replace(/^.+(awstrack\.me|redditmail\.com)\/.+(https:%2F%2F[^/]+).*/i, (...m) => decodeURIComponent(m[2]))
.replace(/^.+(awstrack\.me|redditmail\.com)\/.+(https:%2F%2F[^/]+).*/i, (...m) => decode(m[2]))
.replace(/^.+(www\.google|safelinks\.protection\.outlook\.com|mailchimp\.com).+url=.+$/i,
() => urlGetParam(url, 'url'))
.replace(/^.+click\.godaddy\.com.+$/i, () => urlGetParam(url, 'redir'))
.replace(/^.+delivery-status\.com.+$/i, () => urlGetParam(url, 'fb'))
.replace(/^.+go\.dhlparcel\.nl.+\/([A-Za-z0-9_-]+)$/i, (...m) => base64Url(m[1]))
.replace(/^(.+mopinion\.com.+)\?.*$/i, (...m) => m[1])
@ -73,6 +119,97 @@ const
});
}
return url;
},
cleanCSS = source =>
source.trim()
.replace(/;\s*-[^;]+/g, '')
.replace(/^\s*-[^;]+(;|$)/g, '')
.replace(/white-space[^;]+/g, '')
// Drop Microsoft Office style properties
// .replace(/mso-[^:;]+:[^;]+/gi, '')
,
/*
Parses given css string, and returns css object
keys as selectors and values are css rules
eliminates all css comments before parsing
@param source css string to be parsed
@return object css
*/
parseCSS = source => {
const css = [];
css.toString = () => css.reduce(
(ret, tmp) =>
ret + tmp.selector + ' {\n'
+ (tmp.type === 'media' ? tmp.subStyles.toString() : tmp.rules)
+ '}\n'
,
''
);
/**
* Given css array, parses it and then for every selector,
* prepends namespace to prevent css collision issues
*/
css.applyNamespace = namespace => css.forEach(obj => {
if (obj.type === 'media') {
obj.subStyles.applyNamespace(namespace);
} else {
obj.selector = obj.selector.split(',').map(selector =>
(namespace + ' .mail-body ' + selector.replace(/\./g, '.msg-'))
.replace(/\sbody/gi, '')
).join(',');
}
});
if (source) {
source = source
// strip comments
.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*?@(?:media)[\s\S]*?){([\s\S]*?)}\s*?})|(?:([\s\S]*?){([\s\S]*?)})/gi,
arr;
while (true) {
arr = unified.exec(source);
if (arr === null) {
break;
}
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
.split(/\s+/g).map(item => item.replace(/^(:root|html)$/, '')).join(' ').trim();
// determine the type
if (selector.includes('@media')) {
// we have a media query
css.push({
selector: selector,
type: 'media',
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[4])
});
}
}
}
return css;
};
export const
@ -88,28 +225,32 @@ export const
* @param {string} text
* @returns {string}
*/
cleanHtml = (html, oAttachments) => {
cleanHtml = (html, oAttachments, msgId) => {
let aColor;
const
debug = false, // Config()->Get('debug', 'enable', false);
detectHiddenImages = true, // !!SettingsGet('try_to_detect_hidden_images'),
bqLevel = parseInt(SettingsUserStore.maxBlockquotesLevel()),
minFontSize = parseInt(SettingsUserStore.minFontSize()),
result = {
hasExternals: false
hasExternals: false,
tracking: false,
linkedData: []
},
findAttachmentByCid = cid => oAttachments.findByCid(cid),
findLocationByCid = cid => {
const attachment = findAttachmentByCid(cid);
isMsg = !!msgId,
findAttachmentByCid = cId => oAttachments.findByCid(cId),
findLocationByCid = cId => {
const attachment = findAttachmentByCid(cId);
return attachment?.contentLocation ? attachment : 0;
},
// convert body attributes to CSS
tasks = {
link: value => {
if (/^#[a-fA-Z0-9]{3,6}$/.test(value)) {
tpl.content.querySelectorAll('a').forEach(node => node.style.color || (node.style.color = value))
}
},
link: value => aColor = value,
text: (value, node) => node.style.color = value,
topmargin: (value, node) => node.style.marginTop = pInt(value) + 'px',
leftmargin: (value, node) => node.style.marginLeft = pInt(value) + 'px',
@ -117,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',
@ -144,42 +294,112 @@ export const
'abbr', 'scope',
// td
'colspan', 'rowspan', 'headers'
],
disallowedTags = [
'HEAD','STYLE','SVG','SCRIPT','TITLE','LINK','BASE','META',
'INPUT','OUTPUT','SELECT','BUTTON','TEXTAREA',
'BGSOUND','KEYGEN','SOURCE','OBJECT','EMBED','APPLET','IFRAME','FRAME','FRAMESET','VIDEO','AUDIO','AREA','MAP'
],
nonEmptyTags = [
'A','B','EM','I','SPAN','STRONG'
// others
//'class', 'id', 'target'
];
tpl.innerHTML = html
if (SettingsUserStore.allowStyles()) {
allowedAttributes.push('class');
} else {
msgId = 0;
}
tmpl.innerHTML = html
// .replace(/<pre[^>]*>[\s\S]*?<\/pre>/gi, pre => pre.replace(/\n/g, '\n<br>'))
.replace(/<!doctype[^>]*>/gi, '')
.replace(/<\?xml[^>]*\?>/gi, '')
// 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, '')
// .replace(/<\/?(html|head)[^>]*>/gi, '')
// Fix Reddit https://github.com/the-djmaze/snappymail/issues/540
.replace(/<span class="preview-text"[\s\S]+?<\/span>/, '')
// https://github.com/the-djmaze/snappymail/issues/900
.replace(/\u2028/g,' ')
// https://github.com/the-djmaze/snappymail/issues/1415
.replace(/<br>\s*<\/p>/gi,'</p>')
.trim();
html = '';
// \MailSo\Base\HtmlUtils::ClearComments()
// https://github.com/the-djmaze/snappymail/issues/187
const nodeIterator = document.createNodeIterator(tpl.content, NodeFilter.SHOW_COMMENT);
// Strip all comments
const nodeIterator = document.createNodeIterator(tmpl.content, NodeFilter.SHOW_COMMENT);
while (nodeIterator.nextNode()) {
nodeIterator.referenceNode.remove();
}
tpl.content.querySelectorAll('*').forEach(oElement => {
/**
* Basic support for Linked Data (Structured Email)
* https://json-ld.org/
* https://structured.email/
**/
tmpl.content.querySelectorAll('script[type="application/ld+json"]').forEach(oElement => {
// Could be array of objects or single object
try {
const data = JSON.parse(oElement.textContent);
(isArray(data) ? data : [data]).forEach(entry => result.linkedData.push(entry));
} catch (e) {
console.error(e, oElement.textContent);
}
});
// https://github.com/the-djmaze/snappymail/issues/1125
tmpl.content.querySelectorAll(keepTagContent).forEach(oElement => replaceWithChildren(oElement));
tmpl.content.querySelectorAll(
':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/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,
oStyle = oElement.style;
if ('STYLE' === name) {
let css = msgId ? parseCSS(oElement.textContent) : [];
if (css.length) {
css.applyNamespace(msgId);
css = css.toString();
if (SettingsUserStore.removeColors()) {
css = css.replace(/(background-)?color:[^};]+;?/g, '');
}
oElement.textContent = css;
} else {
oElement.remove();
}
return;
}
if ('XMP' === name) {
const pre = createElement('pre');
pre.innerHTML = encodeHtml(oElement.innerHTML);
oElement.replaceWith(pre);
return;
}
// \MailSo\Base\HtmlUtils::ClearTags()
if (disallowedTags.includes(name)
|| 'none' == oStyle.display
if ('none' == oStyle.display
|| 'hidden' == oStyle.visibility
// || (oStyle.lineHeight && 1 > parseFloat(oStyle.lineHeight)
// || (oStyle.maxHeight && 1 > parseFloat(oStyle.maxHeight)
@ -189,36 +409,31 @@ export const
oElement.remove();
return;
}
/*
// Idea to allow CSS
if ('STYLE' === name) {
msgId = '#rl-msg-061eb4d647771be4185943ce91f0039d';
oElement.textContent = oElement.textContent
.replace(/[^{}]+{/g, m => msgId + ' ' + m.replace(',', ', '+msgId+' '))
.replace(/(background-)color:[^};]+/g, '');
return;
}
*/
const aAttrsForRemove = [],
className = oElement.className,
hasAttribute = name => oElement.hasAttribute(name),
getAttribute = name => hasAttribute(name) ? oElement.getAttribute(name).trim() : '',
setAttribute = (name, value) => oElement.setAttribute(name, value),
delAttribute = name => oElement.removeAttribute(name);
delAttribute = name => {
let value = getAttribute(name);
oElement.removeAttribute(name);
return value;
};
if ('mail-body' === oElement.className) {
forEachObjectEntry(tasks, (name, cb) => {
if (hasAttribute(name)) {
cb(getAttribute(name), oElement);
delAttribute(name);
}
});
if ('mail-body' === className) {
forEachObjectEntry(tasks, (name, cb) =>
hasAttribute(name) && cb(delAttribute(name), oElement)
);
} else if (msgId && className) {
oElement.className = className.replace(/(^|\s+)/g, '$1msg-');
}
if (oElement.hasAttributes()) {
let i = oElement.attributes.length;
while (i--) {
let sAttrName = oElement.attributes[i].name.toLowerCase();
if (!allowedAttributes.includes(sAttrName)) {
if (!allowedAttributes.includes(sAttrName) && ('class' !== sAttrName || 'mail-body' !== className)) {
delAttribute(sAttrName);
aAttrsForRemove.push(sAttrName);
}
@ -230,25 +445,21 @@ export const
// if ('TABLE' === name || 'TD' === name || 'TH' === name) {
if (!oStyle.backgroundImage) {
if ('TD' !== name && 'TH' !== name) {
['width','height'].forEach(key => {
if (hasAttribute(key)) {
value = delAttribute(key);
oStyle[key] || (oStyle[key] = value.includes('%') ? value : value + 'px');
}
});
// Make width responsive
if (hasAttribute('width')) {
value = getAttribute('width');
oStyle.width = value.includes('%') ? value : value + 'px';
delAttribute('width');
}
value = oStyle.removeProperty('width');
if (value) {
value = oStyle.width;
if (100 < parseInt(value,10) && !oStyle.maxWidth) {
oStyle.maxWidth = value;
oStyle.width = '100%';
}
// Make height responsive
if (hasAttribute('height')) {
value = getAttribute('height');
oStyle.height = value.includes('%') ? value : value + 'px';
delAttribute('height');
}
value = oStyle.removeProperty('height');
if (value) {
if (value && !oStyle.maxHeight) {
oStyle.maxHeight = value;
}
}
@ -257,18 +468,23 @@ export const
if ('A' === name) {
value = oElement.href;
if (!/^([a-z]+):/i.test(value)) {
setAttribute('data-x-broken-href', value);
setAttribute('data-x-href-broken', value);
delAttribute('href');
} else {
oElement.href = stripTracking(value);
if (oElement.href != value) {
result.tracking = true;
setAttribute('data-x-href-tracking', value);
}
setAttribute('target', '_blank');
// setAttribute('rel', 'external nofollow noopener noreferrer');
}
setAttribute('tabindex', '-1');
aColor && !oElement.style.color && (oElement.style.color = aColor);
}
// if (['CENTER','FORM'].includes(name)) {
if ('FORM' === name || 'O:P' === name || (nonEmptyTags.includes(name) && ('' == oElement.textContent.trim()))) {
if (nonEmptyTags.includes(name) && ('' == oElement.textContent.trim())) {
('A' !== name || !oElement.querySelector('IMG')) && replaceWithChildren(oElement);
return;
}
@ -281,81 +497,85 @@ export const
*/
let skipStyle = false;
if (hasAttribute('src')) {
value = stripTracking(getAttribute('src'));
delAttribute('src');
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'
|| (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);
}
}
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);
}
else if (httpre.test(value))
{
setAttribute('data-x-src', value);
result.hasExternals = true;
oElement.alt || (oElement.alt = value.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')) {
oStyle.backgroundImage = 'url("' + getAttribute('background') + '")';
delAttribute('background');
oStyle.backgroundImage = 'url("' + delAttribute('background') + '")';
}
if (hasAttribute('bgcolor')) {
oStyle.backgroundColor = getAttribute('bgcolor');
delAttribute('bgcolor');
oStyle.backgroundColor = delAttribute('bgcolor');
}
if (hasAttribute('color')) {
oStyle.color = getAttribute('color');
delAttribute('color');
oStyle.color = delAttribute('color');
}
if (!skipStyle) {
@ -407,8 +627,10 @@ export const
setAttribute('data-x-style-broken-urls', JSON.stringify(urls_broken));
}
if (11 > pInt(oStyle.fontSize)) {
oStyle.removeProperty('font-size');
// https://github.com/the-djmaze/snappymail/issues/1082
if (minFontSize > pInt(oStyle.fontSize)) {
oStyle.fontSize = minFontSize + "px";
// oStyle.removeProperty('font-size');
}
// Removes background and color
@ -420,8 +642,7 @@ export const
oStyle.removeProperty('color');
}
// Drop Microsoft Office style properties
// oStyle.cssText = oStyle.cssText.replace(/mso-[^:;]+:[^;]+/gi, '');
oStyle.cssText && (oStyle.cssText = cleanCSS(oStyle.cssText));
}
if (debug && aAttrsForRemove.length) {
@ -429,8 +650,10 @@ export const
}
});
// return tpl.content.firstChild;
result.html = tpl.innerHTML.trim();
isMsg && blockquoteSwitcher();
// return tmpl.content.firstChild;
result.html = tmpl.innerHTML.trim();
return result;
},
@ -439,9 +662,12 @@ export const
* @returns {string}
*/
htmlToPlain = html => {
if (SettingsUserStore.markdown()) {
return htmlToMarkdown(html);
}
const
hr = '⎯'.repeat(64),
forEach = (selector, fn) => tpl.content.querySelectorAll(selector).forEach(fn),
forEach = (selector, fn) => tmpl.content.querySelectorAll(selector).forEach(fn),
blockquotes = node => {
let bq;
while ((bq = node.querySelector('blockquote'))) {
@ -459,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, ' ');
@ -469,10 +697,12 @@ export const
html = html.replace(/\n*<\/(div|tr)(\s[\s\S]*?)?>\n*/gi, '\n');
}
tpl.innerHTML = html
tmpl.innerHTML = html
.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`));
@ -509,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 + ' '
);
});
@ -519,16 +751,21 @@ export const
forEach('i,em', i => i.replaceWith(`*${i.textContent}*`));
// Convert line-breaks
tpl.innerHTML = tpl.innerHTML
tmpl.innerHTML = tmpl.innerHTML
.replace(/\n{3,}/gm, '\n\n')
.replace(/\n<br[^>]*>/g, '\n')
.replace(/<br[^>]*>\n/g, '\n');
forEach('br', br => br.replaceWith('\n'));
// Blockquotes must be last
blockquotes(tpl.content);
blockquotes(tmpl.content);
return (tpl.content.textContent || '').trim();
return (tmpl.content.textContent || '').trim();
},
htmlToMarkdown = html => {
tmpl.innerHTML = html;
return turndown.turndown(tmpl.content);
},
/**
@ -539,7 +776,9 @@ export const
plainToHtml = plain => {
plain = plain.toString()
.replace(/\r/g, '')
.replace(/^>[> ]>+/gm, ([match]) => (match ? match.replace(/[ ]+/g, '') : match));
.replace(/^>[> ]>+/gm, ([match]) => (match ? match.replace(/[ ]+/g, '') : match))
// https://github.com/the-djmaze/snappymail/issues/900
.replace(/\u2028/g,' ');
let bIn = false,
bDo = true,
@ -580,200 +819,28 @@ export const
aText = aNextText;
} while (bDo);
return aText.join('\n')
tmpl.innerHTML = aText.join('\n')
// .replace(/~~~\/blockquote~~~\n~~~blockquote~~~/g, '\n')
.replace(/&/g, '&amp;')
.replace(/>/g, '&gt;')
.replace(/</g, '&lt;')
.replace(url, (...m) => {
m[2] = stripTracking(m[2]);
return `${m[1]}<a href="${m[2]}" target="_blank">${m[2]}</a>`;
.replace(urlRegExp, (...m) => {
m[0] = stripTracking(m[0]);
return `<a href="${m[0]}" target="_blank">${m[0]}</a>`;
})
.replace(email, '$1<a href="mailto:$2">$2</a>')
.replace(tel, '$1<a href="$2">$2</a>')
.replace(tel, '<a href="$1">$1</a>')
.replace(/~~~blockquote~~~\s*/g, '<blockquote>')
.replace(/\s*~~~\/blockquote~~~/g, '</blockquote>')
.replace(/\n/g, '<br>');
},
WYSIWYGS = ko.observableArray();
WYSIWYGS.push(['Squire', (owner, container, onReady)=>{
let squire = new SquireUI(container);
setTimeout(()=>onReady(squire), 1);
/*
squire.on('blur', () => owner.blurTrigger());
squire.on('focus', () => clearTimeout(owner.blurTimer));
squire.on('mode', () => {
owner.blurTrigger();
owner.onModeChange?.(!owner.isPlain());
});
*/
}]);
rl.registerWYSIWYG = (name, construct) => WYSIWYGS.push([name, construct]);
export class HtmlEditor {
/**
* @param {Object} element
* @param {Function=} onBlur
* @param {Function=} onReady
* @param {Function=} onModeChange
*/
constructor(element, onBlur = null, onReady = null, onModeChange = null) {
this.blurTimer = 0;
this.onBlur = onBlur;
this.onModeChange = onModeChange;
if (element) {
onReady = onReady ? [onReady] : [];
this.onReady = fn => onReady.push(fn);
// TODO: make 'which' user configurable
// const which = 'CKEditor4',
// wysiwyg = WYSIWYGS.find(item => which == item[0]) || WYSIWYGS.find(item => 'Squire' == item[0]);
const wysiwyg = WYSIWYGS.find(item => 'Squire' == item[0]);
wysiwyg[1](this, element, editor => {
this.editor = editor;
editor.on('blur', () => this.blurTrigger());
editor.on('focus', () => clearTimeout(this.blurTimer));
editor.on('mode', () => {
this.blurTrigger();
this.onModeChange?.(!this.isPlain());
});
this.onReady = fn => fn();
onReady.forEach(fn => fn());
});
}
}
blurTrigger() {
if (this.onBlur) {
clearTimeout(this.blurTimer);
this.blurTimer = setTimeout(() => this.onBlur?.(), 200);
}
}
/**
* @returns {boolean}
*/
isHtml() {
return this.editor ? !this.isPlain() : false;
}
/**
* @returns {boolean}
*/
isPlain() {
return this.editor ? 'plain' === this.editor.mode : false;
}
/**
* @returns {void}
*/
clearCachedSignature() {
this.onReady(() => this.editor.execCommand('insertSignature', {
clearCache: true
}));
}
/**
* @param {string} signature
* @param {bool} html
* @param {bool} insertBefore
* @returns {void}
*/
setSignature(signature, html, insertBefore = false) {
this.onReady(() => this.editor.execCommand('insertSignature', {
isHtml: html,
insertBefore: insertBefore,
signature: signature
}));
}
/**
* @param {boolean=} wrapIsHtml = false
* @returns {string}
*/
getData() {
let result = '';
if (this.editor) {
try {
if (this.isPlain() && this.editor.plugins.plain && this.editor.__plain) {
result = this.editor.__plain.getRawData();
} else {
result = this.editor.getData();
}
} catch (e) {} // eslint-disable-line no-empty
}
return result;
}
/**
* @returns {string}
*/
getDataWithHtmlMark() {
return (this.isHtml() ? ':HTML:' : '') + this.getData();
}
modeWysiwyg() {
this.onReady(() => this.editor.setMode('wysiwyg'));
}
modePlain() {
this.onReady(() => this.editor.setMode('plain'));
}
setHtmlOrPlain(text) {
text.startsWith(':HTML:')
? this.setHtml(text.slice(6))
: this.setPlain(text);
}
setData(mode, data) {
this.onReady(() => {
const editor = this.editor;
this.clearCachedSignature();
try {
editor.setMode(mode);
if (this.isPlain() && editor.plugins.plain && editor.__plain) {
editor.__plain.setRawData(data);
} else {
editor.setData(data);
}
} catch (e) { console.error(e); }
});
}
setHtml(html) {
this.setData('wysiwyg', html/*.replace(/<p[^>]*><\/p>/gi, '')*/);
}
setPlain(txt) {
this.setData('plain', txt);
}
focus() {
this.onReady(() => this.editor.focus());
}
hasFocus() {
try {
return !!this.editor?.focusManager.hasFocus;
} catch (e) {
return false;
}
}
blur() {
this.onReady(() => this.editor.focusManager.blur(true));
}
clear() {
this.onReady(() => this.isPlain() ? this.setPlain('') : this.setHtml(''));
}
}
blockquoteSwitcher();
return tmpl.innerHTML.trim();
};
rl.Utils = {
cleanHtml: cleanHtml,
htmlToPlain: htmlToPlain,
plainToHtml: plainToHtml
plainToHtml: plainToHtml,
htmlToMarkdown: htmlToMarkdown
// markdownToHtml: md => marked.parse(md)
};

161
dev/Common/HtmlEditor.js Normal file
View file

@ -0,0 +1,161 @@
import { SettingsUserStore } from 'Stores/User/Settings';
export const
WYSIWYGS = ko.observableArray();
WYSIWYGS.push({
name: 'Squire',
construct: (owner, container, onReady) => onReady(new SquireUI(container))
});
rl.registerWYSIWYG = (name, construct) => WYSIWYGS.push({name, construct});
export class HtmlEditor {
/**
* @param {Object} element
* @param {Function=} onBlur
* @param {Function=} onReady
* @param {Function=} onModeChange
*/
constructor(element, onReady = null, onModeChange = null, onBlur = null) {
this.blurTimer = 0;
this.onBlur = onBlur;
this.onModeChange = onModeChange;
if (element) {
onReady = onReady ? [onReady] : [];
this.onReady = fn => onReady.push(fn);
const which = SettingsUserStore.editorWysiwyg(),
wysiwyg = WYSIWYGS.find(item => which == item.name) || WYSIWYGS.find(item => 'Squire' == item.name);
wysiwyg.construct(this, element, editor => setTimeout(()=>{
this.editor = editor;
editor.on('blur', () => this.blurTrigger());
editor.on('focus', () => clearTimeout(this.blurTimer));
editor.on('mode', () => {
this.blurTrigger();
this.onModeChange?.(!this.isPlain());
});
this.onReady = fn => fn();
onReady.forEach(fn => fn());
},1));
}
}
blurTrigger() {
if (this.onBlur) {
clearTimeout(this.blurTimer);
this.blurTimer = setTimeout(() => this.onBlur?.(), 200);
}
}
/**
* @returns {boolean}
*/
isHtml() {
return this.editor ? !this.isPlain() : false;
}
/**
* @returns {boolean}
*/
isPlain() {
return this.editor ? 'plain' === this.editor.mode : false;
}
/**
* @returns {void}
*/
clearCachedSignature() {
this.onReady(() => this.editor.execCommand('insertSignature', {
clearCache: true
}));
}
/**
* @param {string} signature
* @param {bool} html
* @param {bool} insertBefore
* @returns {void}
*/
setSignature(signature, html, insertBefore = false) {
this.onReady(() => this.editor.execCommand('insertSignature', {
isHtml: html,
insertBefore: insertBefore,
signature: signature
}));
}
/**
* @param {boolean=} wrapIsHtml = false
* @returns {string}
*/
getData() {
let result = '';
if (this.editor) {
try {
if (this.isPlain()) {
result = this.editor.getPlainData();
} else {
result = this.editor.getData();
}
} catch (e) {} // eslint-disable-line no-empty
}
return result;
}
/**
* @returns {string}
*/
getDataWithHtmlMark() {
return (this.isHtml() ? ':HTML:' : '') + this.getData();
}
modeWysiwyg() {
this.onReady(() => this.editor.setMode('wysiwyg'));
}
modePlain() {
this.onReady(() => this.editor.setMode('plain'));
}
setHtmlOrPlain(text) {
text.startsWith(':HTML:')
? this.setHtml(text.slice(6))
: this.setPlain(text);
}
setData(mode, data) {
this.onReady(() => {
const editor = this.editor;
this.clearCachedSignature();
try {
editor.setMode(mode);
if (this.isPlain()) {
editor.setPlainData(data);
} else {
editor.setData(data);
}
} catch (e) { console.error(e); }
});
}
setHtml(html) {
this.setData('wysiwyg', html/*.replace(/<p[^>]*><\/p>/gi, '')*/);
}
setPlain(txt) {
this.setData('plain', txt);
}
focus() {
this.onReady(() => this.editor.focus());
}
blur() {
this.onReady(() => this.editor.blur());
}
clear() {
this.onReady(() => this.isPlain() ? this.setPlain('') : this.setHtml(''));
}
}

View file

@ -1,13 +1,13 @@
import { pInt } from 'Common/Utils';
import { Settings } from 'Common/Globals';
import { doc, Settings, SettingsAdmin } from 'Common/Globals';
const
BASE = doc.location.pathname.replace(/\/+$/,'') + '/',
HASH_PREFIX = '#/',
SERVER_PREFIX = './?',
adminPath = () => rl.adminArea() && !Settings.app('adminHostUse'),
adminPath = () => rl.adminArea() && !SettingsAdmin('host'),
prefix = () => SERVER_PREFIX + (adminPath() ? Settings.app('adminPath') : '');
prefix = () => BASE + '?' + (adminPath() ? SettingsAdmin('path') : '');
export const
SUB_QUERY_PREFIX = '&q[]=',
@ -21,7 +21,7 @@ export const
/**
* @returns {string}
*/
logoutLink = () => adminPath() ? prefix() : './',
logoutLink = () => adminPath() ? prefix() : BASE,
/**
* @param {string} type
@ -30,23 +30,24 @@ export const
* @returns {string}
*/
serverRequestRaw = (type, hash) =>
SERVER_PREFIX + '/Raw/' + SUB_QUERY_PREFIX + '/'
+ '0/' // Settings.get('AccountHash') ?
BASE + '?/Raw/' + SUB_QUERY_PREFIX + '/'
+ '0/' // Settings.get('accountHash') ?
+ (type
? type + '/' + (hash ? SUB_QUERY_PREFIX + '/' + hash : '')
: ''),
/**
* @param {string} download
* @param {string=} customSpecSuffix
* @returns {string}
*/
attachmentDownload = (download, customSpecSuffix) =>
serverRequestRaw('Download', download, customSpecSuffix),
attachmentDownload = (download) =>
serverRequestRaw('Download', download),
proxy = url =>
SERVER_PREFIX + '/ProxyExternal/'
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')),
/**
* @param {string} type
@ -54,13 +55,16 @@ export const
*/
serverRequest = type => prefix() + '/' + type + '/' + SUB_QUERY_PREFIX + '/0/',
// Is '?/Css/0/Admin' needed?
cssLink = theme => BASE + '?/Css/0/User/-/' + encodeURI(theme) + '/-/' + Date.now() + '/Hash/-/Json/',
/**
* @param {string} lang
* @param {boolean} isAdmin
* @returns {string}
*/
langLink = (lang, isAdmin) =>
SERVER_PREFIX + '/Lang/0/' + (isAdmin ? 'Admin' : 'App')
BASE + '?/Lang/0/' + (isAdmin ? 'Admin' : 'App')
+ '/' + encodeURI(lang)
+ '/' + Settings.app('version') + '/',
@ -75,6 +79,10 @@ export const
* @returns {string}
*/
themePreviewLink = theme => {
if (theme.endsWith('@nextcloud')) {
theme = theme.slice(0, theme.length - 10).trim();
return parent.OC.webroot + '/themes/' + encodeURI(theme) + '/snappymail/preview.png';
}
let path = 'webVersionPath';
if (theme.endsWith('@custom')) {
theme = theme.slice(0, theme.length - 7).trim();

View file

@ -7,7 +7,7 @@ import { koComputable } from 'External/ko';
oCallbacks:
ItemSelect
MiddleClick
AutoSelect
canSelect
ItemGetUid
UpOrDown
*/
@ -21,15 +21,13 @@ export class Selector {
* @param {koProperty} koFocusedItem
* @param {string} sItemSelector
* @param {string} sItemCheckedSelector
* @param {string} sItemFocusedSelector
*/
constructor(
koList,
koSelectedItem,
koFocusedItem,
sItemSelector,
sItemCheckedSelector,
sItemFocusedSelector
sItemCheckedSelector
) {
koFocusedItem = (koFocusedItem || ko.observable(null)).extend({ toggleSubscribeProperty: [this, 'focused'] });
koSelectedItem = (koSelectedItem || ko.observable(null)).extend({ toggleSubscribeProperty: [null, 'selected'] });
@ -46,7 +44,7 @@ export class Selector {
this.sItemSelector = sItemSelector;
this.sItemCheckedSelector = sItemCheckedSelector;
this.sItemFocusedSelector = sItemFocusedSelector;
this.sItemFocusedSelector = sItemSelector + '.focused';
this.sLastUid = '';
this.oCallbacks = {};
@ -65,8 +63,8 @@ export class Selector {
this.listChecked.subscribe(items => {
if (items.length) {
koSelectedItem() ? koSelectedItem(null) : koSelectedItem.valueHasMutated?.();
} else if (this.autoSelect()) {
koSelectedItem(koFocusedItem());
} else {
this.autoSelect();
}
});
@ -74,7 +72,7 @@ export class Selector {
koSelectedItem.subscribe(item => {
if (item) {
koList.forEach(subItem => subItem.checked(false));
// koList.forEach(subItem => subItem.checked(false));
selectedItemUseCallback && itemSelectedThrottle(item);
} else {
selectedItemUseCallback && itemSelected();
@ -87,8 +85,7 @@ export class Selector {
* Below code is used to keep checked/focused/selected states when array is refreshed.
*/
let aCache = [],
aCheckedCache = [],
let aCheckedCache = [],
mFocused = null,
mSelected = null;
@ -99,7 +96,6 @@ export class Selector {
items.forEach(item => {
const uid = this.getItemUid(item);
if (uid) {
aCache.push(uid);
item.checked() && aCheckedCache.push(uid);
if (!mFocused && item.focused()) {
mFocused = uid;
@ -118,12 +114,12 @@ export class Selector {
koList.subscribe(aItems => {
selectedItemUseCallback = false;
koFocusedItem(null);
koSelectedItem(null);
this.unselect();
if (isArray(aItems)) {
let temp,
isChecked;
isChecked,
next = this.iFocusedNextHelper || this.iSelectNextHelper;
aItems.forEach(item => {
const uid = this.getItemUid(item);
@ -148,24 +144,10 @@ export class Selector {
selectedItemUseCallback = true;
if (
(this.iSelectNextHelper || this.iFocusedNextHelper) &&
aItems.length &&
!koFocusedItem()
) {
temp = null;
if (this.iFocusedNextHelper) {
temp = aItems[-1 === this.iFocusedNextHelper ? aItems.length - 1 : 0];
}
if (!temp && this.iSelectNextHelper) {
temp = aItems[-1 === this.iSelectNextHelper ? aItems.length - 1 : 0];
}
if (next && aItems.length && !koFocusedItem()) {
temp = aItems[-1 === next ? aItems.length - 1 : 0];
if (temp) {
if (this.iSelectNextHelper) {
koSelectedItem(temp);
}
this.iSelectNextHelper && koSelectedItem(temp);
koFocusedItem(temp);
@ -177,12 +159,9 @@ export class Selector {
this.iFocusedNextHelper = 0;
}
if (this.autoSelect() && !isChecked && !koSelectedItem()) {
koSelectedItem(koFocusedItem());
}
!isChecked && !koSelectedItem() && this.autoSelect();
}
aCache = [];
aCheckedCache = [];
mFocused = null;
mSelected = null;
@ -206,10 +185,11 @@ export class Selector {
addEventsListeners(contentScrollable, {
click: event => {
let el = event.target.closestWithin(this.sItemSelector, contentScrollable);
el && this.actionClick(ko.dataFor(el), event);
const el = event.target.closestWithin(this.sItemSelector, contentScrollable);
let item = el && ko.dataFor(el);
el && (this.oCallbacks.click || (()=>1))(event, item) && this.actionClick(item, event);
const item = getItem(this.sItemCheckedSelector);
item = getItem(this.sItemCheckedSelector);
if (item) {
if (event.shiftKey) {
this.actionClick(item, event);
@ -254,8 +234,10 @@ export class Selector {
/**
* @returns {boolean}
*/
autoSelect() {
return (this.oCallbacks.AutoSelect || (()=>1))() && this.focusedItem();
autoSelect(bForce) {
(bForce || (this.oCallbacks.canSelect || (()=>1))())
&& this.focusedItem()
&& this.selectedItem(this.focusedItem());
}
/**
@ -272,10 +254,11 @@ export class Selector {
* @param {boolean=} bForceSelect = false
*/
newSelectPosition(sEventKey, bShiftKey, bForceSelect) {
let isArrow = 'ArrowUp' === sEventKey || 'ArrowDown' === sEventKey,
result;
let result;
const pageStep = 10,
const up = 'ArrowUp' === sEventKey,
isArrow = up || 'ArrowDown' === sEventKey,
pageStep = 10,
list = this.list(),
listLen = list.length,
focused = this.focusedItem();
@ -287,8 +270,7 @@ export class Selector {
} else if (listLen) {
if (focused) {
if (isArrow) {
let i = list.indexOf(focused),
up = 'ArrowUp' == sEventKey;
let i = list.indexOf(focused);
if (bShiftKey) {
shiftStart = -1 < shiftStart ? shiftStart : i;
shiftStart == i
@ -331,9 +313,7 @@ export class Selector {
if (result) {
this.focusedItem(result);
if ((this.autoSelect() || bForceSelect) && !this.list.hasChecked()) {
this.selectedItem(result);
}
!this.list.hasChecked() && this.autoSelect(bForceSelect);
this.scrollToFocused();
}
}

View file

@ -1,5 +1,5 @@
import ko from 'ko';
import { Notification, UploadErrorCode } from 'Common/Enums';
import { Notifications, UploadErrorCode } from 'Common/Enums';
import { langLink } from 'Common/Links';
import { doc, createElement } from 'Common/Globals';
import { getKeyByValue, forEachObjectEntry } from 'Common/Utils';
@ -12,8 +12,8 @@ const
init = () => {
if (rl.I18N) {
I18N_DATA = rl.I18N;
Date.defineRelativeTimeFormat(rl.relativeTime || {});
rl.I18N = null;
doc.documentElement.dir = I18N_DATA.LANG_DIR;
return 1;
}
},
@ -21,13 +21,39 @@ const
i18nKey = key => key.replace(/([a-z])([A-Z])/g, '$1_$2').toUpperCase(),
getNotificationMessage = code => {
let key = getKeyByValue(Notification, code);
return key ? I18N_DATA.NOTIFICATIONS[i18nKey(key).replace('_NOTIFICATION', '_ERROR')] : '';
};
let key = getKeyByValue(Notifications, code);
return key ? I18N_DATA.NOTIFICATIONS[key] : '';
},
fromNow = date => relativeTime(Math.round((date.getTime() - Date.now()) / 1000));
export const
translateTrigger = ko.observable(false),
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/RelativeTimeFormat
// see /snappymail/v/0.0.0/app/localization/relativetimeformat/
relativeTime = seconds => {
let unit = 'second',
t = [[60,'minute'],[3600,'hour'],[86400,'day'],[2628000,'month'],[31536000,'year']],
i = 5,
abs = Math.abs(seconds);
while (i--) {
if (t[i][0] <= abs) {
seconds = Math.round(seconds / t[i][0]);
unit = t[i][1];
break;
}
}
if (Intl.RelativeTimeFormat) {
return (new Intl.RelativeTimeFormat(doc.documentElement.lang)).format(seconds, unit);
}
// Safari < 14
abs = Math.abs(seconds);
let rtf = rl.relativeTime.long[unit][0 > seconds ? 'past' : 'future'],
plural = rl.relativeTime.plural(abs);
return (rtf[plural] || rtf).replace('{0}', abs);
},
/**
* @param {string} key
* @param {Object=} valueList
@ -35,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;
@ -75,30 +101,33 @@ export const
timestampToString = (timeStampInUTC, formatStr) => {
const now = Date.now(),
time = 0 < timeStampInUTC ? Math.min(now, timeStampInUTC * 1000) : (0 === timeStampInUTC ? now : 0);
time = 0 < timeStampInUTC ? timeStampInUTC * 1000 : (0 === timeStampInUTC ? now : 0);
if (31536000000 < time) {
const m = new Date(time);
const m = new Date(time), h = LanguageStore.hourCycle();
switch (formatStr) {
case 'FROMNOW':
return m.fromNow();
case 'SHORT': {
if (4 >= (now - time) / 3600000)
return m.fromNow();
const mt = m.getTime(), date = new Date,
return fromNow(m);
case 'AUTO': {
// 4 hours
if (14400000 >= now - time)
return fromNow(m);
const date = new Date,
dt = date.setHours(0,0,0,0);
if (mt > dt)
return i18n('MESSAGE_LIST/TODAY_AT', {TIME: m.format('LT',0,LanguageStore.hourCycle())});
if (mt > dt - 86400000)
return i18n('MESSAGE_LIST/YESTERDAY_AT', {TIME: m.format('LT',0,LanguageStore.hourCycle())});
if (date.getFullYear() === m.getFullYear())
return m.format('d M');
return m.format('LL',0,LanguageStore.hourCycle());
return (time > dt - 86400000)
? i18n(
time > dt ? 'MESSAGE_LIST/TODAY_AT' : 'MESSAGE_LIST/YESTERDAY_AT',
{TIME: m.format('LT',0,h)}
)
: m.format(
date.getFullYear() === m.getFullYear()
? {day: '2-digit', month: 'short', hour: 'numeric', minute: 'numeric'}
: {dateStyle: 'medium', timeStyle: 'short'}
, 0, h);
}
case 'FULL':
return m.format('LLL',0,LanguageStore.hourCycle());
return m.format('LLL',0,h);
default:
return m.format(formatStr,0,LanguageStore.hourCycle());
return m.format(formatStr,0,h);
}
}
@ -108,18 +137,17 @@ export const
timeToNode = (element, time) => {
try {
if (time) {
element.dateTime = (new Date(time * 1000)).format('Y-m-d\\TH:i:s');
element.dateTime = new Date(time * 1000).toISOString();
} else {
time = Date.parse(element.dateTime) / 1000;
}
let key = element.dataset.momentFormat;
let key = element.dataset.timeFormat;
if (key) {
element.textContent = timestampToString(time, key);
}
if ((key = element.dataset.momentFormatTitle)) {
element.title = timestampToString(time, key);
if ('FULL' !== key && 'FROMNOW' !== key) {
element.title = timestampToString(time, 'FULL');
}
}
} catch (e) {
// prevent knockout crashes
@ -127,9 +155,7 @@ export const
}
},
reloadTime = () => setTimeout(() =>
doc.querySelectorAll('time').forEach(element => timeToNode(element))
, 1),
reloadTime = () => doc.querySelectorAll('time').forEach(element => timeToNode(element)),
/**
* @param {Function} startCallback
@ -149,7 +175,7 @@ export const
*/
getNotification = (code, message = '', defCode = 0) => {
code = pInt(code);
if (Notification.ClientViewError === code && message) {
if (Notifications.ClientViewError === code && message) {
return message;
}
@ -158,6 +184,9 @@ export const
|| '';
},
getErrorMessage = (code, data) =>
getNotification(code) || data?.messageAdditional || data?.message || data,
/**
* @param {*} code
* @returns {string}
@ -171,27 +200,26 @@ export const
* @param {boolean} admin
* @param {string} language
*/
translatorReload = (admin, language) =>
translatorReload = (language, admin) =>
new Promise((resolve, reject) => {
const script = createElement('script');
script.onload = () => {
// reload the data
if (init()) {
i18nToNodes(doc);
admin || reloadTime();
translateTrigger(!translateTrigger());
// admin || reloadTime();
}
script.remove();
resolve();
};
script.onerror = () => reject(new Error('Language '+language+' failed'));
script.onerror = () => reject(Error('Language '+language+' failed'));
script.src = langLink(language, admin);
// script.async = true;
doc.head.append(script);
}),
/**
*
* @param {string} language
* @param {boolean=} isEng = false
* @returns {string}
@ -201,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,19 +10,22 @@ 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) =>
item && undefined !== item.disabled && domItem?.classList.toggle('disabled', domItem.disabled = item.disabled),
// unescape(encodeURIComponent()) makes the UTF-16 DOMString to an UTF-8 string
b64EncodeJSON = data => btoa(unescape(encodeURIComponent(JSON.stringify(data)))),
b64Encode = data => btoa(unescape(encodeURIComponent(data))),
/* // Without deprecated 'unescape':
b64EncodeJSON = data => btoa(encodeURIComponent(JSON.stringify(data)).replace(
b64Encode = data => btoa(encodeURIComponent(data).replace(
/%([0-9A-F]{2})/g, (match, p1) => String.fromCharCode('0x' + p1)
)),
*/
b64EncodeJSON = data => b64Encode(JSON.stringify(data)),
b64EncodeJSONSafe = data => b64EncodeJSON(data).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''),
getKeyByValue = (o, v) => Object.keys(o).find(key => o[key] === v);

View file

@ -1,26 +1,68 @@
import { MessageFlagsCache } from 'Common/Cache';
import { Notification } from 'Common/Enums';
import { Notifications } from 'Common/Enums';
import { MessageSetAction, ComposeType/*, FolderType*/ } from 'Common/EnumsUser';
import { doc, createElement, elementById, dropdowns, dropdownVisibility, SettingsGet, leftPanelDisabled } from 'Common/Globals';
import { plainToHtml } from 'Common/Html';
import { getNotification } from 'Common/Translator';
import { EmailModel } from 'Model/Email';
import { MessageModel } from 'Model/Message';
import { EmailCollectionModel } from 'Model/EmailCollection';
import { MessageUserStore } from 'Stores/User/Message';
import { MessagelistUserStore } from 'Stores/User/Messagelist';
import { SettingsUserStore } from 'Stores/User/Settings';
import * as Local from 'Storage/Client';
import { ThemeStore } from 'Stores/Theme';
import Remote from 'Remote/User/Fetch';
import { attachmentDownload } from 'Common/Links';
import { AccountModel } from 'Model/Account';
import { IdentityModel } from 'Model/Identity';
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);
Remote.request('AccountsAndIdentities', (iError, oData) => {
AccountUserStore.loading(false);
IdentityUserStore.loading(false);
if (!iError) {
let items = oData.Result.Accounts;
AccountUserStore(isArray(items)
? items.map(oValue => new AccountModel(oValue.email, oValue.name))
: []
);
AccountUserStore.unshift(new AccountModel(SettingsGet('mainEmail'), '', false));
items = oData.Result.Identities;
IdentityUserStore(isArray(items)
? 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);
}
});
},
/**
* @param {string} link
* @returns {boolean}
@ -42,6 +84,29 @@ download = (link, name = "") => {
}
},
downloadZip = (name, hashes, onError, fTrigger, folder) => {
if (hashes.length) {
let params = {
target: 'zip',
filename: name,
hashes: hashes
};
if (!onError) {
onError = () => alert('Download failed');
}
if (folder) {
params.folder = folder;
// params.uids = uids;
}
Remote.post('AttachmentsActions', fTrigger || null, params)
.then(result => {
let hash = result?.Result?.fileHash;
hash ? download(attachmentDownload(hash), hash+'.zip') : onError();
})
.catch(onError);
}
},
/**
* @returns {function}
*/
@ -50,14 +115,15 @@ computedPaginatorHelper = (koCurrentPage, koPageCount) => {
const currentPage = koCurrentPage(),
pageCount = koPageCount(),
result = [],
lang = doc.documentElement.lang,
fAdd = (index, push = true, customName = '') => {
const data = {
current: index === currentPage,
name: customName ? customName.toString() : index.toString(),
custom: !!customName,
title: customName ? index.toString() : '',
value: index.toString()
};
const name = index.toLocaleString(lang),
data = {
current: index === currentPage,
name: customName || name,
title: customName ? name : '',
value: index
};
push ? result.push(data) : result.unshift(data);
};
@ -66,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;
@ -133,29 +199,15 @@ mailToHelper = mailToUrl => {
mailToUrl = mailToUrl.slice(7).split('?');
const
email = mailToUrl[0],
email = decodeURIComponent(mailToUrl[0]),
params = new URLSearchParams(mailToUrl[1]),
toEmailModel = value => null != value ? EmailModel.parseEmailLine(value) : null;
to = params.get('to'),
toEmailModel = value => EmailCollectionModel.fromString(value);
showMessageComposer([
ComposeType.Empty,
null,
params.get('to')
? Object.values(
toEmailModel(email + ',' + params.get('to')).reduce((result, value) => {
if (value) {
if (result[value.email]) {
if (!result[value.email].name) {
result[value.email] = value;
}
} else {
result[value.email] = value;
}
}
return result;
}, {})
)
: EmailModel.parseEmailLine(email),
toEmailModel(to ? email + ',' + to : email),
toEmailModel(params.get('cc')),
toEmailModel(params.get('bcc')),
params.get('subject'),
@ -173,37 +225,24 @@ showMessageComposer = (params = []) =>
rl.app.showMessageComposer(params);
},
setLayoutResizer = (source, target, sClientSideKeyName, mode) =>
setLayoutResizer = (source, sClientSideKeyName, mode) =>
{
if (source.layoutResizer && source.layoutResizer.mode != mode) {
target.removeAttribute('style');
source.removeAttribute('style');
}
source.observer?.disconnect();
// source.classList.toggle('resizable', mode);
if (mode) {
const length = Local.get(sClientSideKeyName + mode) || SettingsGet('Resizer' + sClientSideKeyName + mode),
setTargetPos = mode => {
let value;
if ('Width' == mode) {
value = source.offsetWidth;
target.style.left = value + 'px';
} else {
value = source.offsetHeight;
target.style.top = (4 + source.offsetTop + value) + 'px';
}
return value;
};
const length = Local.get(sClientSideKeyName + mode) || SettingsGet('Resizer' + sClientSideKeyName + mode);
if (length) {
source.style[mode.toLowerCase()] = length + 'px';
setTargetPos(mode);
}
if (!source.layoutResizer) {
const resizer = createElement('div', {'class':'resizer'}),
save = (data => Remote.saveSettings(0, data)).debounce(500),
size = {},
store = () => {
const value = setTargetPos(resizer.mode),
const value = ('Width' == resizer.mode) ? source.offsetWidth : source.offsetHeight,
prop = resizer.key + resizer.mode;
(value == Local.get(prop)) || Local.set(prop, value);
(value == SettingsGet('Resizer' + prop)) || save({['Resizer' + prop]: value});
@ -250,114 +289,86 @@ setLayoutResizer = (source, target, sClientSideKeyName, mode) =>
}
},
viewMessage = (oMessage, popup) => {
if (popup) {
oMessage.popupMessage();
} else {
MessageUserStore.error('');
let id = 'rl-msg-' + oMessage.hash,
body = oMessage.body || elementById(id);
if (!body) {
body = createElement('div',{
id:id,
hidden:'',
class:'b-text-part'
+ (oMessage.pgpSigned() ? ' openpgp-signed' : '')
+ (oMessage.pgpEncrypted() ? ' openpgp-encrypted' : '')
+ (oMessage.smimeSigned() ? ' smime-signed' : '')
+ (oMessage.smimeEncrypted() ? ' smime-encrypted' : '')
});
MessageUserStore.purgeCache();
}
body.message = oMessage;
oMessage.body = body;
if (!SettingsUserStore.viewHTML() || !oMessage.viewHtml()) {
oMessage.viewPlain();
}
MessageUserStore.bodiesDom().append(body);
MessageUserStore.loading(false);
oMessage.body.hidden = false;
if (oMessage.isUnseen() && SettingsUserStore.messageReadAuto()) {
MessageUserStore.MessageSeenTimer = setTimeout(
() => MessagelistUserStore.setAction(oMessage.folder, MessageSetAction.SetSeen, [oMessage]),
SettingsUserStore.messageReadDelay() * 1000 // seconds
);
}
}
},
populateMessageBody = (oMessage, popup) => {
if (oMessage) {
popup || MessageUserStore.message(oMessage);
popup || MessageUserStore.loading(true);
Remote.message((iError, oData/*, bCached*/) => {
if (iError) {
if (Notification.RequestAborted !== iError && !popup) {
MessageUserStore.message(null);
MessageUserStore.error(getNotification(iError));
}
} else {
let json = oData?.Result;
if (
json &&
MessageModel.validJson(json) &&
oMessage.folder === json.Folder
) {
const threads = oMessage.threads(),
isNew = !popup && oMessage.uid != json.Uid && threads.includes(json.Uid),
messagesDom = MessageUserStore.bodiesDom();
if (isNew) {
oMessage = MessageModel.reviveFromJson(json);
if (oMessage) {
oMessage.threads(threads);
MessageFlagsCache.initMessage(oMessage);
// Set clone
oMessage = MessageModel.fromMessageListItem(oMessage);
}
MessageUserStore.message(oMessage);
if (oMessage.body) {
viewMessage(oMessage, popup);
} else {
popup || MessageUserStore.loading(true);
Remote.message((iError, oData/*, bCached*/) => {
if (iError) {
if (Notifications.RequestAborted !== iError && !popup) {
MessageUserStore.message(null);
MessageUserStore.error(getNotification(iError));
}
if (oMessage && oMessage.uid == json.Uid) {
popup || MessageUserStore.error('');
} else {
let json = oData?.Result;
if (json
&& ((
oMessage.hash && oMessage.hash === json.hash
) || (
!oMessage.hash
&& oMessage.folder === json.folder
&& oMessage.uid == json.uid)
)
&& oMessage.revivePropertiesFromJson(json)
) {
/*
if (bCached) {
delete json.Flags;
delete json.flags;
}
oMessage.body.remove();
*/
isNew || oMessage.revivePropertiesFromJson(json);
if (messagesDom) {
let id = 'rl-msg-' + oMessage.hash.replace(/[^a-zA-Z0-9]/g, ''),
body = elementById(id);
if (body) {
oMessage.body = body;
oMessage.isHtml(body.classList.contains('html'));
oMessage.hasImages(body.rlHasImages);
} else {
body = Element.fromHTML('<div id="' + id + '" hidden="" class="b-text-part '
+ (oMessage.pgpSigned() ? ' openpgp-signed' : '')
+ (oMessage.pgpEncrypted() ? ' openpgp-encrypted' : '')
+ '">'
+ '</div>');
oMessage.body = body;
if (!SettingsUserStore.viewHTML() || !oMessage.viewHtml()) {
oMessage.viewPlain();
}
MessageUserStore.purgeMessageBodyCache();
}
messagesDom.append(body);
popup || (oMessage.body.hidden = false);
popup && oMessage.viewPopupMessage();
}
MessageFlagsCache.initMessage(oMessage);
if (oMessage.isUnseen()) {
MessageUserStore.MessageSeenTimer = setTimeout(
() => MessagelistUserStore.setAction(oMessage.folder, MessageSetAction.SetSeen, [oMessage]),
SettingsUserStore.messageReadDelay() * 1000 // seconds
);
}
if (isNew) {
let selectedMessage = MessagelistUserStore.selectedMessage();
if (
selectedMessage &&
(oMessage.folder !== selectedMessage.folder || oMessage.uid != selectedMessage.uid)
) {
MessagelistUserStore.selectedMessage(null);
if (1 === MessagelistUserStore.length) {
MessagelistUserStore.focusedMessage(null);
}
} else if (!selectedMessage) {
selectedMessage = MessagelistUserStore.find(
subMessage =>
subMessage &&
subMessage.folder === oMessage.folder &&
subMessage.uid == oMessage.uid
);
if (selectedMessage) {
MessagelistUserStore.selectedMessage(selectedMessage);
MessagelistUserStore.focusedMessage(selectedMessage);
}
}
}
viewMessage(oMessage, popup);
}
}
}
popup || MessageUserStore.loading(false);
}, oMessage.folder, oMessage.uid);
popup || MessageUserStore.loading(false);
}, oMessage.folder, oMessage.uid);
}
}
};
leftPanelDisabled.subscribe(value => value && moveAction(false));
leftPanelDisabled.subscribe(value => value && moveAction(0));
moveAction.subscribe(value => value && leftPanelDisabled(false));

View file

@ -1,10 +1,12 @@
export class CheckboxComponent {
constructor(params = {}) {
this.name = params.name;
this.value = ko.isObservable(params.value) ? params.value
: 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

@ -1,8 +1,30 @@
import { doc, createElement, addEventsListeners } from 'Common/Globals';
import { EmailModel } from 'Model/Email';
import { addressparser } from 'Mime/Address';
const contentType = 'snappymail/emailaddress',
getAddressKey = li => li?.emailaddress?.key;
getAddressKey = li => li?.emailaddress?.key,
parseEmailLine = line => addressparser(line).map(item =>
(item.name || item.email)
? new EmailModel(item.email, item.name) : null
).filter(v => v),
splitEmailLine = line => {
const result = [];
let exists = false;
addressparser(line).forEach(item => {
const address = (item.name || item.email)
? new EmailModel(item.email, item.name)
: null;
if (address?.email) {
exists = true;
}
result.push(address ? address.toLine() : item.name);
});
return exists ? result : null;
};
let dragAddress, datalist;
@ -144,7 +166,9 @@ export class EmailAddressesComponent {
_parseInput(force) {
let val = this.input.value;
if ((force || val.includes(',') || val.includes(';')) && this._parseValue(val)) {
if ((force || val.includes(',') || val.includes(';')
|| (val.charAt(val.length-1)===' ' && this._simpleEmailMatch(val)))
&& this._parseValue(val)) {
this.input.value = '';
}
this._resizeInput();
@ -154,8 +178,8 @@ export class EmailAddressesComponent {
if (val) {
const self = this,
v = val.trim(),
hook = (v && [',', ';', '\n'].includes(v.slice(-1))) ? EmailModel.splitEmailLine(val) : null,
values = (hook || [val]).map(value => EmailModel.parseEmailLine(value))
hook = (v && [',', ';', '\n'].includes(v.slice(-1))) ? splitEmailLine(val) : null,
values = (hook || [val]).map(value => parseEmailLine(value))
.flat(Infinity)
.map(item => (item.toLine ? [item.toLine(), item] : [item, null]));
@ -263,6 +287,13 @@ export class EmailAddressesComponent {
}
}
_simpleEmailMatch(value) {
// A very SIMPLE test to check if the value might be an email
const val = value.trim();
return /^[^@]*<[^\s@]{1,128}@[^\s@]{1,256}\.[\w]{2,32}>$/g.test(val)
|| /^[^\s@]{1,128}@[^\s@]{1,256}\.[\w]{2,32}$/g.test(val);
}
_renderTags() {
let self = this;
[...self.ul.children].forEach(node => node !== self.inputCont && node.remove());

View file

@ -15,7 +15,7 @@ export class JCard {
if (input) {
// read from jCard
if (typeof input !== 'object') {
throw new Error('error reading vcard')
throw Error('error reading vcard')
}
this.parseFromJCard(input)
}
@ -87,7 +87,7 @@ export class JCard {
arg = new VCardProperty(String(arg), value, params, type);
}
if (!(arg instanceof VCardProperty)) {
throw new Error('invalid argument of VCard.set(), expects string arguments or a VCardProperty');
throw Error('invalid argument of VCard.set(), expects string arguments or a VCardProperty');
}
let field = arg.getField();
this.props.set(field, [arg]);
@ -101,7 +101,7 @@ export class JCard {
arg = new VCardProperty(String(arg), value, params, type);
}
if (!(arg instanceof VCardProperty)) {
throw new Error('invalid argument of VCard.add(), expects string arguments or a VCardProperty');
throw Error('invalid argument of VCard.add(), expects string arguments or a VCardProperty');
}
// VCardProperty arguments
let field = arg.getField();
@ -124,15 +124,15 @@ 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)))
throw new Error("Attempted to remove VCardProperty VCard does not have: ".concat(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)
this.props.delete(arg.getField());
}
// incorrect arguments
else
throw new Error('invalid argument of VCard.remove(), expects ' +
throw Error('invalid argument of VCard.remove(), expects ' +
'string and optional param filter or a VCardProperty');
}
@ -202,7 +202,7 @@ export class JCard {
parseFullName(options) {
let n = this.getOne('n');
if (n === undefined) {
throw new Error('\'fn\' VCardProperty not present in card, cannot parse full name');
throw Error('\'fn\' VCardProperty not present in card, cannot parse full name');
}
let fnString = '';
// Position in n -> position in fn

View file

@ -55,7 +55,7 @@ export class VCardProperty {
}
// invalid property
else {
throw new Error('invalid Property constructor');
throw Error('invalid Property constructor');
}
}

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() + ' + ',
@ -14,65 +10,43 @@ const
createElement = name => doc.createElement(name),
tpl = createElement('template'),
clr = createElement('input'),
trimLines = html => html.trim().replace(/^(<div>\s*<br\s*\/?>\s*<\/div>)+/, '').trim(),
htmlToPlain = html => rl.Utils.htmlToPlain(html).trim(),
plainToHtml = text => rl.Utils.plainToHtml(text),
getFragmentOfChildren = parent => {
let frag = doc.createDocumentFragment();
frag.append(...parent.childNodes);
return frag;
},
forEachObjectValue = (obj, fn) => Object.values(obj).forEach(fn),
SquireDefaultConfig = {
/*
blockTag: 'P',
undo: {
documentSizeThreshold: -1, // -1 means no threshold
undoLimit: -1 // -1 means no limit
},
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;
}
};
clr.type = "color";
clr.style.display = 'none';
doc.body.append(clr);
class SquireUI
{
constructor(container) {
const
doClr = name => () => {
clr = createElement('input'),
doClr = name => input => {
// https://github.com/the-djmaze/snappymail/issues/826
clr.style.left = (input.offsetLeft + input.parentNode.offsetLeft) + 'px';
clr.style.width = input.offsetWidth + 'px';
clr.value = '';
clr.onchange = () => squire.setStyle({[name]:clr.value});
clr.click();
// Chrome 110+ https://github.com/the-djmaze/snappymail/issues/1199
// clr.oninput = () => squire.setStyle({[name]:clr.value});
setTimeout(()=>clr.click(),1);
},
actions = {
@ -84,8 +58,7 @@ class SquireUI
[i18n('SETTINGS_GENERAL/EDITOR_HTML'),'wysiwyg'],
[i18n('SETTINGS_GENERAL/EDITOR_PLAIN'),'plain']
],
cmd: s => this.setMode('plain' == s.value ? 'plain' : 'wysiwyg'),
hint: i18n('EDITOR/TEXT_SWITCHER_PLAIN_TEXT', 'Plain')
cmd: s => this.setMode('plain' == s.value ? 'plain' : 'wysiwyg')
}
},
font: {
@ -111,20 +84,45 @@ class SquireUI
cmd: s => squire.setStyle({ fontFamily: s.value })
},
fontSize: {
select: ['11px','13px','16px','20px','24px','30px'],
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: 0,
},
// dir: {
// select: [
// [i18n('EDITOR/DIR_LTR', 'LTR'),'ltr'],
// [i18n('EDITOR/DIR_RTL', 'RTL'),'rtl'],
// [i18n('EDITOR/DIR_AUTO', 'Auto'),'auto'],
// ['',''],
// ],
// cmd: s => {
// squire.setAttribute('dir', s.value || null);
// // squire.setStyle({ 'unicode-bidi': 'plaintext' });
// }
// }
},
dir: {
dir_ltr: {
html: '⁋',
cmd: () => squire.setTextDirection('ltr')
},
dir_rtl: {
html: '¶',
cmd: () => squire.setTextDirection('rtl')
}
},
colors: {
textColor: {
html: 'A<sub>▾</sub>',
cmd: doClr('color'),
hint: 'Text color'
cmd: doClr('color')
},
backgroundColor: {
html: '🎨', /* ▧ */
cmd: doClr('backgroundColor'),
hint: 'Background color'
cmd: doClr('backgroundColor')
},
},
inline: {
@ -132,37 +130,37 @@ class SquireUI
html: 'B',
cmd: () => this.doAction('bold'),
key: 'B',
hint: 'Bold'
matches: 'B,STRONG'
},
italic: {
html: 'I',
cmd: () => this.doAction('italic'),
key: 'I',
hint: 'Italic'
matches: 'I'
},
underline: {
html: '<u>U</u>',
cmd: () => this.doAction('underline'),
key: 'U',
hint: 'Underline'
matches: 'U'
},
strike: {
html: '<s>S</s>',
cmd: () => this.doAction('strikethrough'),
key: 'Shift + 7',
hint: 'Strikethrough'
matches: 'S'
},
sub: {
html: 'Xₙ',
cmd: () => this.doAction('subscript'),
key: 'Shift + 5',
hint: 'Subscript'
matches: 'SUB'
},
sup: {
html: 'Xⁿ',
cmd: () => this.doAction('superscript'),
key: 'Shift + 6',
hint: 'Superscript'
matches: 'SUP'
}
},
block: {
@ -170,13 +168,13 @@ class SquireUI
html: '#',
cmd: () => this.doList('OL'),
key: 'Shift + 8',
hint: 'Ordered list'
matches: 'OL'
},
ul: {
html: '⋮',
cmd: () => this.doList('UL'),
key: 'Shift + 9',
hint: 'Unordered list'
matches: 'UL'
},
quote: {
html: '"',
@ -184,19 +182,17 @@ class SquireUI
let parent = squire.getSelectionClosest('UL,OL,BLOCKQUOTE')?.nodeName;
('BLOCKQUOTE' == parent) ? squire.decreaseQuoteLevel() : squire.increaseQuoteLevel();
},
hint: 'Blockquote'
matches: 'BLOCKQUOTE'
},
indentDecrease: {
html: '⇤',
cmd: () => squire.changeIndentationLevel('decrease'),
key: ']',
hint: 'Decrease indent'
key: ']'
},
indentIncrease: {
html: '⇥',
cmd: () => squire.changeIndentationLevel('increase'),
key: '[',
hint: 'Increase indent'
key: '['
}
},
targets: {
@ -209,21 +205,21 @@ class SquireUI
url.length ? squire.makeLink(url) : (node && squire.removeLink());
}
},
hint: 'Link'
matches: 'A'
},
imageUrl: {
html: '🖼️',
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();
},
hint: 'Image URL'
matches: 'IMG'
},
imageUpload: {
html: '📂️',
cmd: () => browseImage.click(),
hint: 'Image select',
matches: 'IMG'
}
},
/*
@ -235,19 +231,26 @@ class SquireUI
undo: {
html: '↶',
cmd: () => squire.undo(),
key: 'Z',
hint: 'Undo'
key: 'Z'
},
redo: {
html: '↷',
cmd: () => squire.redo(),
key: 'Y',
hint: 'Redo'
key: 'Y'
},
source: {
html: '👁',
cmd: () => this.setMode('source' == this.mode ? 'wysiwyg' : 'source'),
hint: i18n('EDITOR/TEXT_SWITCHER_SOURCE', 'Source')
cmd: btn => {
this.setMode('source' == this.mode ? 'wysiwyg' : 'source');
btn.classList.toggle('active', 'source' == this.mode);
}
}
},
clear: {
removeStyle: {
html: '⎚',
cmd: () => squire.setStyle()
}
}
},
@ -258,6 +261,30 @@ class SquireUI
browseImage = createElement('input'),
squire = new Squire(wysiwyg, SquireDefaultConfig);
clr.type = 'color';
toolbar.append(clr);
// Chrome https://github.com/the-djmaze/snappymail/issues/1199
let clrid = 'squire-colors',
colorlist = doc.getElementById(clrid),
add = hex => colorlist.append(new Option(hex));
if (!colorlist) {
colorlist = createElement('datalist');
colorlist.id = clrid;
// Color blind safe Tableau 10 by Maureen Stone
add('#4E79A7');
add('#F28E2B');
add('#E15759');
add('#76B7B2');
add('#59A14F');
add('#EDC948');
add('#B07AA1');
add('#FF9DA7');
add('#9C755F');
add('#BAB0AC');
doc.body.append(colorlist);
}
clr.setAttribute('list', clrid);
browseImage.type = 'file';
browseImage.accept = 'image/*';
browseImage.style.display = 'none';
@ -271,25 +298,18 @@ class SquireUI
plain.className = 'squire-plain';
wysiwyg.className = 'squire-wysiwyg';
wysiwyg.dir = 'auto';
this.mode = ''; // 'plain' | 'wysiwyg'
this.__plain = {
getRawData: () => this.plain.value,
setRawData: plain => this.plain.value = plain
};
this.container = container;
this.squire = squire;
this.plain = plain;
this.wysiwyg = wysiwyg;
dispatchEvent(new CustomEvent('squire-toolbar', {detail:{squire:this,actions:actions}}));
toolbar.className = 'squire-toolbar btn-toolbar';
let group, action/*, touchTap*/;
for (group in actions) {
/*
if ('bidi' == group && !rl.settings.app('allowHtmlEditorBitiButtons')) {
continue;
}
*/
let toolgroup = createElement('div');
toolgroup.className = 'btn-group';
toolgroup.id = 'squire-toolgroup-'+group;
@ -310,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;
@ -318,7 +339,7 @@ class SquireUI
option.style[action] = value;
group.append(option);
});
input.append(group);
input.add(group);
});
}
ev = 'input';
@ -342,6 +363,7 @@ class SquireUI
*/
}
input.addEventListener(ev, () => cfg.cmd(input));
cfg.hint = i18n('EDITOR/' + action.toUpperCase());
if (cfg.hint) {
input.title = cfg.key ? cfg.hint + ' (' + ctrlKey + cfg.key + ')' : cfg.hint;
} else if (cfg.key) {
@ -359,36 +381,178 @@ 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;
// squire.addEventListener('focus', () => shortcuts.off());
// squire.addEventListener('blur', () => shortcuts.on());
container.append(toolbar, wysiwyg, plain);
/*
squire.addEventListener('dragover', );
squire.addEventListener('drop', );
squire.addEventListener('pathChange', );
squire.addEventListener('cursor', );
squire.addEventListener('select', );
squire.addEventListener('input', );
squire.addEventListener('willPaste', );
squire.addEventListener( 'keydown keyup', monitorShiftKey )
squire.addEventListener( 'keydown', onKey )
*/
/**
* @param {string} fontName
* @return {string}
*/
const normalizeFontName = (fontName) => fontName.trim().replace(/(^["']*|["']*$)/g, '').trim().toLowerCase();
// CKEditor gimmicks used by HtmlEditor
this.plugins = {
plain: true
};
this.focusManager = {
hasFocus: () => squire._isFocused,
blur: () => squire.blur()
/** @type {string[]} - lower cased array of available font families*/
const fontFamiliesLowerCase = Object.values(actions.font.fontFamily.input.options).map(option => option.value.toLowerCase());
/**
* A theme might have CSS like div.squire-wysiwyg[contenteditable="true"] {
* font-family: 'Times New Roman', Times, serif; }
* so let's find the best match squire.getRoot()'s font
* it will also help to properly handle generic font names like 'sans-serif'
* @type {number}
*/
let defaultFontFamilyIndex = 0;
const squireRootFonts = getComputedStyle(squire.getRoot()).fontFamily.split(',').map(normalizeFontName);
fontFamiliesLowerCase.some((family, index) => {
const matchFound = family.split(',').some(availableFontName => {
const normalizedFontName = normalizeFontName(availableFontName);
return squireRootFonts.some(squireFontName => squireFontName === normalizedFontName);
});
if (matchFound) {
defaultFontFamilyIndex = index;
}
return matchFound;
});
/**
* Instead of comparing whole 'font-family' strings,
* we are going to look for individual font names, because we might be
* editing a Draft started in another email client for example
*
* @type {Object.<string,number>}
*/
const fontNamesMap = {};
/**
* @param {string} fontFamily
* @param {number} index
*/
const processFontFamilyString = (fontFamily, index) => {
fontFamily.split(',').forEach(fontName => {
const key = normalizeFontName(fontName);
if (fontNamesMap[key] === undefined) {
fontNamesMap[key] = index;
}
});
};
// first deal with the default font family
processFontFamilyString(fontFamiliesLowerCase[defaultFontFamilyIndex], defaultFontFamilyIndex);
// and now with the rest of the font families
fontFamiliesLowerCase.forEach((fontFamily, index) => {
if (index !== defaultFontFamilyIndex) {
processFontFamilyString(fontFamily, index);
}
});
// -----
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 => {
// 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 (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 familyFound = false;
let sizeFound = false;
do {
if (!familyFound && elm.style.fontFamily) {
familyFound = true;
familySelectedIndex = -1; // show empty select if we don't know the font
const fontNames = elm.style.fontFamily.split(',');
for (let i = 0; i < fontNames.length; i++) {
const index = fontNamesMap[normalizeFontName(fontNames[i])];
if (index !== undefined) {
familySelectedIndex = index;
break;
}
}
}
if (!sizeFound && elm.style.fontSize) {
sizeFound = true;
// -1 is ok because it will just show a black <select>
sizeSelectedIndex = actions.font.fontSize.select.indexOf(elm.style.fontSize);
}
elm = elm.parentElement;
} while ((!familyFound || !sizeFound) && elm && elm !== squireRoot);
actions.font.fontFamily.input.selectedIndex = familySelectedIndex;
actions.font.fontSize.input.selectedIndex = sizeSelectedIndex;
}
});
/*
squire.addEventListener('cursor', e => {
console.dir({cursor:e.detail.range});
});
squire.addEventListener('select', e => {
console.dir({select:e.detail.range});
});
*/
}
doAction(name) {
@ -401,11 +565,11 @@ class SquireUI
fn = {UL:'makeUnorderedList',OL:'makeOrderedList'};
(parent == type) ? this.squire.removeList() : this.squire[fn[type]]();
}
/*
testPresenceinSelection(format, validation) {
return validation.test(this.squire.getPath()) || this.squire.hasFormat(format);
}
*/
setMode(mode) {
if (this.mode != mode) {
let cl = this.container.classList, source = 'source' == this.mode;
@ -426,7 +590,6 @@ class SquireUI
this.modeSelect.selectedIndex = 'plain' == this.mode ? 1 : 0;
}
// CKeditor gimmicks used by HtmlEditor
on(type, fn) {
if ('mode' == type) {
this.onModeChange = fn;
@ -469,6 +632,7 @@ class SquireUI
// Move cursor above signature
div.before(br);
div.before(br.cloneNode());
// squire._docWasChanged();
}
this._prev_txt_sig = signature;
} catch (e) {
@ -492,6 +656,18 @@ class SquireUI
squire.setSelection( range );
}
getPlainData() {
return this.plain.value;
}
setPlainData(text) {
this.plain.value = text;
}
blur() {
this.squire.blur();
}
focus() {
if ('plain' == this.mode) {
this.plain.focus();

View file

@ -1,12 +1,12 @@
import 'External/ko';
import ko from 'ko';
import { HtmlEditor } from 'Common/Html';
import { RFC822 } from 'Common/File';
import { HtmlEditor } from 'Common/HtmlEditor';
import { timeToNode } from 'Common/Translator';
import { doc, elementById, addEventsListeners, dropdowns, leftPanelDisabled } from 'Common/Globals';
import { dropdownsDetectVisibility } from 'Common/UtilsUser';
import { EmailAddressesComponent } from 'Component/EmailAddresses';
import { ThemeStore } from 'Stores/Theme';
import { moveMessagesToFolder, dropFilesInFolder } from 'Common/Folders';
import { dropFilesInFolder } from 'Common/Folders';
import { setExpandedFolder } from 'Model/FolderCollection';
import { FolderUserStore } from 'Stores/User/Folder';
import { MessagelistUserStore } from 'Stores/User/Messagelist';
@ -44,7 +44,7 @@ const rlContentType = 'snappymail/action',
let files = false;
// if (e.dataTransfer.types.includes('Files'))
for (const item of e.dataTransfer.items) {
files |= 'file' === item.kind && 'message/rfc822' === item.type;
files |= 'file' === item.kind && RFC822 === item.type;
}
if (files || dragMessages()) {
e.stopPropagation();
@ -63,7 +63,9 @@ const rlContentType = 'snappymail/action',
dragDrop = (e, element, folder, dragData) => {
dragStop(e, element);
if (dragMessages() && 'copyMove' == e.dataTransfer.effectAllowed) {
moveMessagesToFolder(FolderUserStore.currentFolderFullName(), dragData.data, folder.fullName, e.ctrlKey);
MessagelistUserStore.moveMessages(
FolderUserStore.currentFolderFullName(), dragData.data, folder.fullName, e.ctrlKey
);
} else if (e.dataTransfer.types.includes('Files')) {
dropFilesInFolder(folder.fullName, e.dataTransfer.files);
}
@ -89,19 +91,19 @@ Object.assign(ko.bindingHandlers, {
};
if (ko.isObservable(fValue)) {
editor = new HtmlEditor(element, fUpdateKoValue, fOnReady, fUpdateKoValue);
editor = new HtmlEditor(element, fOnReady, fUpdateKoValue, fUpdateKoValue);
fValue.__fetchEditorValue = fUpdateKoValue;
fValue.subscribe(fUpdateEditorValue);
// ko.utils.domNodeDisposal.addDisposeCallback(element, () => {
// ko.addDisposeCallback(element, () => {
// });
}
}
},
moment: {
time: {
init: ttn,
update: ttn
},
@ -192,10 +194,6 @@ Object.assign(ko.bindingHandlers, {
};
addEventsListeners(element, {
dragstart: e => {
dragData = {
action: 'sortable',
element: element
};
setDragAction(e, 'sortable', 'move', element, element);
element.style.opacity = 0.25;
},
@ -245,18 +243,5 @@ Object.assign(ko.bindingHandlers, {
dropdowns.push(element);
element.ddBtn = new BSN.Dropdown(element.querySelector('.dropdown-toggle'));
}
},
openDropdownTrigger: {
update: (element, fValueAccessor) => {
if (ko.unwrap(fValueAccessor())) {
const el = element.ddBtn;
el.open || el.toggle();
// el.focus();
dropdownsDetectVisibility();
fValueAccessor()(false);
}
}
}
});

76
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) =>
@ -28,6 +29,22 @@ export const
dispose = disposable => isFunction(disposable?.dispose) && disposable.dispose(),
onEvent = (element, event, fn) => {
element.addEventListener(event, fn);
ko.addDisposeCallback(element, () => element.removeEventListener(event, fn));
},
onKey = (key, element, fValueAccessor, fAllBindings, model) => {
let fn = event => {
if (key == event.key) {
// stopEvent(event);
// element.dispatchEvent(new Event('change'));
fValueAccessor().call(model);
}
};
onEvent(element, 'keydown', fn);
},
// With this we don't need delegateRunOnDestroy
koArrayWithDestroy = data => {
data = ko.observableArray(data);
@ -50,46 +67,31 @@ Object.assign(ko.bindingHandlers, {
},
update: (element, fValueAccessor) => {
let value = ko.unwrap(fValueAccessor());
value = isFunction(value) ? value() : value;
errorTip(element, value);
errorTip(element, isFunction(value) ? value() : value);
}
},
onEnter: {
init: (element, fValueAccessor, fAllBindings, viewModel) => {
let fn = event => {
if ('Enter' == event.key) {
element.dispatchEvent(new Event('change'));
fValueAccessor().call(viewModel);
}
};
element.addEventListener('keydown', fn);
ko.utils.domNodeDisposal.addDisposeCallback(element, () => element.removeEventListener('keydown', fn));
}
init: (element, fValueAccessor, fAllBindings, model) =>
onKey('Enter', element, fValueAccessor, fAllBindings, model)
},
onEsc: {
init: (element, fValueAccessor, fAllBindings, viewModel) => {
let fn = event => {
if ('Escape' == event.key) {
element.dispatchEvent(new Event('change'));
fValueAccessor().call(viewModel);
}
};
element.addEventListener('keyup', fn);
ko.utils.domNodeDisposal.addDisposeCallback(element, () => element.removeEventListener('keyup', fn));
}
init: (element, fValueAccessor, fAllBindings, model) =>
onKey('Escape', element, fValueAccessor, fAllBindings, model)
},
onSpace: {
init: (element, fValueAccessor, fAllBindings, viewModel) => {
let fn = event => {
if (' ' == event.key) {
fValueAccessor().call(viewModel, event);
}
};
element.addEventListener('keyup', fn);
ko.utils.domNodeDisposal.addDisposeCallback(element, () => element.removeEventListener('keyup', fn));
init: (element, fValueAccessor, fAllBindings, model) =>
onKey(' ', element, fValueAccessor, fAllBindings, model)
},
toggle: {
init: (element, fValueAccessor) => {
let observable = fValueAccessor(),
fn = () => observable(!observable());
onEvent(element, 'click', fn);
onEvent(element, 'keydown', event => ' ' == event.key && fn());
}
},
@ -100,16 +102,12 @@ Object.assign(ko.bindingHandlers, {
}
},
title: {
update: (element, fValueAccessor) => element.title = ko.unwrap(fValueAccessor())
},
command: {
init: (element, fValueAccessor, fAllBindings, viewModel, bindingContext) => {
const command = fValueAccessor();
if (!command || !command.canExecute) {
throw new Error('Value should be a command');
throw Error('Value should be a command');
}
ko.bindingHandlers['FORM'==element.nodeName ? 'submit' : 'click'].init(
@ -121,10 +119,8 @@ Object.assign(ko.bindingHandlers, {
);
},
update: (element, fValueAccessor) => {
const cl = element.classList;
let disabled = !fValueAccessor().canExecute();
cl.toggle('disabled', disabled);
element.classList.toggle('disabled', disabled);
if (element.matches('INPUT,TEXTAREA,BUTTON')) {
element.disabled = disabled;
@ -180,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,46 +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;
}
</style>
</head>
<body></body>
</html>

View file

@ -5,9 +5,13 @@ function typeCast(curValue, newValue) {
if (null != curValue) {
switch (typeof curValue)
{
case 'boolean': return 0 != newValue && !!newValue;
case 'number': return isFinite(newValue) ? parseFloat(newValue) : 0;
case 'string': return null != newValue ? '' + newValue : '';
case 'boolean':
return 0 != newValue && !!newValue;
case 'number':
newValue = parseFloat(newValue);
return isFinite(newValue) ? newValue : 0;
case 'string':
return null != newValue ? '' + newValue : '';
case 'object':
if (curValue.constructor.reviveFromJson) {
return curValue.constructor.reviveFromJson(newValue);
@ -23,10 +27,10 @@ export class AbstractModel {
constructor() {
/*
if (new.target === AbstractModel) {
throw new Error("Can't instantiate AbstractModel!");
throw Error("Can't instantiate AbstractModel!");
}
*/
this.disposables = [];
Object.defineProperty(this, 'disposables', {value: []});
}
addObservables(observables) {
@ -80,13 +84,11 @@ export class AbstractModel {
}
revivePropertiesFromJson(json) {
let model = this.constructor;
if (!model.validJson(json)) {
return false;
}
forEachObjectEntry(json, (key, value) => {
const model = this.constructor,
valid = model.validJson(json);
valid && forEachObjectEntry(json, (key, value) => {
if ('@' !== key[0]) try {
key = key[0].toLowerCase() + key.slice(1);
// key = key[0].toLowerCase() + key.slice(1);
switch (typeof this[key])
{
case 'function':
@ -103,9 +105,11 @@ export class AbstractModel {
this[key] = typeCast(this[key], value);
break;
case 'undefined':
console.log(`Undefined ${model.name}.${key} set`);
this[key] = value;
// fall through
default:
break;
// default:
// console.log((typeof this[key])+` ${model.name}.${key} not revived`);
// console.log((typeof this[key])+' '+(model.name)+'.'+key+' not revived');
}
} catch (e) {
@ -113,7 +117,7 @@ export class AbstractModel {
console.error(e);
}
});
return true;
return valid;
}
}

View file

@ -1,7 +1,7 @@
import ko from 'ko';
import { addObservablesTo, addComputablesTo, addSubscribablesTo } from 'External/ko';
import { keyScope, addShortcut, SettingsGet, leftPanelDisabled, toggleLeftPanel, elementById } from 'Common/Globals';
import { keyScope, addShortcut, SettingsGet, toggleLeftPanel, elementById } from 'Common/Globals';
import { ViewTypePopup, showScreenPopup } from 'Knoin/Knoin';
import { SaveSettingStatus } from 'Common/Enums';
@ -59,10 +59,10 @@ export class AbstractViewPopup extends AbstractView
super('Popups' + name, ViewTypePopup);
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
});
@ -78,8 +78,6 @@ export class AbstractViewPopup extends AbstractView
afterShow() {} // Happens after showModal() animation transitionend
onHide() {} // Happens before animation transitionend
afterHide() {} // Happens after animation transitionend
close() {}
*/
}
@ -96,7 +94,6 @@ export class AbstractViewLeft extends AbstractView
constructor(templateID)
{
super(templateID, 'left');
this.leftPanelDisabled = leftPanelDisabled;
this.toggleLeftPanel = toggleLeftPanel;
}
}
@ -118,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),
@ -133,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);
}
);
@ -140,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

@ -1,9 +1,12 @@
import ko from 'ko';
import { koComputable } from 'External/ko';
import { doc, $htmlCL, elementById, fireEvent } from 'Common/Globals';
import { doc, $htmlCL, elementById, createElement, fireEvent } from 'Common/Globals';
import { forEachObjectEntry } from 'Common/Utils';
import { i18nToNodes } from 'Common/Translator';
import { leftPanelDisabled } from 'Common/Globals';
import { ThemeStore } from 'Stores/Theme';
let
currentScreen = null,
defaultScreenName = '';
@ -22,13 +25,13 @@ const
screen = screenName => (screenName && SCREENS.get(screenName)) || null,
/**
* Creates the extended AbstractView model
* @param {Function} ViewModelClass
* @param {Object=} vmScreen
* @returns {*}
*/
buildViewModel = (ViewModelClass, vmScreen) => {
if (ViewModelClass && !ViewModelClass.__builded) {
let vmDom = null;
if (ViewModelClass && !ViewModelClass.__vm) {
const
vm = new ViewModelClass(vmScreen),
id = vm.viewModelTemplateID,
@ -36,35 +39,30 @@ const
dialog = ViewTypePopup === vm.viewType,
vmPlace = doc.getElementById(position);
ViewModelClass.__builded = true;
ViewModelClass.__vm = vm;
if (vmPlace) {
vmDom = Element.fromHTML(dialog
? '<dialog id="V-'+ id + '"></dialog>'
: '<div id="V-'+ id + '" hidden=""></div>');
ViewModelClass.__vm = vm;
let vmDom = dialog
? createElement('dialog',{id:'V-'+id})
: createElement('div',{id:'V-'+id,hidden:''})
vmPlace.append(vmDom);
vm.viewModelDom = ViewModelClass.__dom = vmDom;
vm.viewModelDom = vmDom;
if (dialog) {
vm.close = () => hideScreenPopup(ViewModelClass);
// Firefox < 98 / Safari < 15.4 HTMLDialogElement not defined
if (!vmDom.showModal) {
vmDom.className = 'polyfill';
vmDom.showModal = () => {
vmDom.backdrop ||
vmDom.before(vmDom.backdrop = Element.fromHTML('<div class="dialog-backdrop"></div>'));
vmDom.before(vmDom.backdrop = createElement('div',{class:'dialog-backdrop'}));
vmDom.setAttribute('open','');
vmDom.open = true;
vmDom.returnValue = null;
vmDom.backdrop.hidden = false;
};
vmDom.close = v => {
vmDom.close = () => {
// if (vmDom.dispatchEvent(new CustomEvent('cancel', {cancelable:true}))) {
vmDom.backdrop.hidden = true;
vmDom.returnValue = v;
vmDom.removeAttribute('open', null);
vmDom.open = false;
// vmDom.dispatchEvent(new CustomEvent('close'));
@ -76,14 +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')) {
autofocus(vmDom);
vm.afterShow?.();
fireEvent('rl-vm-visible', vm);
} else {
vmDom.close();
vm.afterHide?.();
// fireEvent('rl-vm-hidden', vm);
}
}
};
@ -98,6 +99,7 @@ const
vmDom.backdrop.style.zIndex = 3000 + (visiblePopups.size * 2);
}
vm.keyScope.set();
setTimeout(()=>autofocus(vmDom),1);
requestAnimationFrame(() => { // wait just before the next paint
vmDom.offsetHeight; // force a reflow
vmDom.classList.add('animate'); // trigger the transitions
@ -138,10 +140,9 @@ const
screen.viewModels.forEach(ViewModelClass => {
if (
ViewModelClass.__vm &&
ViewModelClass.__dom &&
ViewTypePopup !== ViewModelClass.__vm.viewType
) {
fn(ViewModelClass.__vm, ViewModelClass.__dom);
fn(ViewModelClass.__vm, ViewModelClass.__vm.viewModelDom);
}
});
},
@ -151,85 +152,69 @@ const
forEachViewModel(screenToHide, (vm, dom) => {
dom.hidden = true;
vm.onHide?.();
destroy && vm.viewModelDom.remove();
destroy && dom.remove();
});
ThemeStore.isMobile() && leftPanelDisabled(true);
},
/**
* @param {Function} ViewModelClassToHide
* @returns {void}
*/
hideScreenPopup = ViewModelClassToHide =>
ViewModelClassToHide?.__vm && ViewModelClassToHide.__dom
&& ViewModelClassToHide.__vm.modalVisible(false),
/**
* @param {string} screenName
* @param {string} subPart
* @returns {void}
*/
screenOnRoute = (screenName, subPart) => {
let vmScreen = null,
isSameScreen = false;
if (null == screenName || '' == screenName) {
screenName = defaultScreenName;
}
if (fireEvent('sm-show-screen', screenName, 1)) {
screenName = screenName || defaultScreenName;
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();
}
if (screenName) {
vmScreen = screen(screenName);
if (!vmScreen) {
vmScreen = screen(defaultScreenName);
if (vmScreen) {
subPart = screenName + '/' + subPart;
screenName = defaultScreenName;
}
let vmScreen = screen(screenName);
if (!vmScreen) {
vmScreen = screen(defaultScreenName);
if (vmScreen) {
subPart = screenName + '/' + subPart;
screenName = defaultScreenName;
}
}
if (vmScreen?.__started) {
let isSameScreen = currentScreen && vmScreen === currentScreen;
if (!vmScreen.__builded) {
vmScreen.__builded = true;
vmScreen.viewModels.forEach(ViewModelClass =>
buildViewModel(ViewModelClass, vmScreen)
);
vmScreen.onBuild?.();
}
if (vmScreen?.__started) {
isSameScreen = currentScreen && vmScreen === currentScreen;
setTimeout(() => {
// hide screen
currentScreen && !isSameScreen && hideScreen(currentScreen);
// --
if (!vmScreen.__builded) {
vmScreen.__builded = true;
currentScreen = vmScreen;
vmScreen.viewModels.forEach(ViewModelClass =>
buildViewModel(ViewModelClass, vmScreen)
);
// show screen
if (!isSameScreen) {
vmScreen.onShow?.();
vmScreen.onBuild?.();
forEachViewModel(vmScreen, (vm, dom) => {
vm.beforeShow?.();
i18nToNodes(dom);
dom.hidden = false;
vm.onShow?.();
autofocus(dom);
});
}
// --
setTimeout(() => {
// hide screen
currentScreen && !isSameScreen && hideScreen(currentScreen);
// --
currentScreen = vmScreen;
// show screen
if (!isSameScreen) {
vmScreen.onShow?.();
forEachViewModel(vmScreen, (vm, dom) => {
vm.beforeShow?.();
i18nToNodes(dom);
dom.hidden = false;
vm.onShow?.();
autofocus(dom);
});
}
// --
vmScreen.__cross?.parse(subPart);
}, 1);
}
vmScreen.__cross?.parse(subPart);
}, 1);
}
}
};
@ -244,15 +229,11 @@ export const
* @returns {void}
*/
showScreenPopup = (ViewModelClassToShow, params = []) => {
const vm = buildViewModel(ViewModelClassToShow) && ViewModelClassToShow.__dom && ViewModelClassToShow.__vm;
const vm = buildViewModel(ViewModelClassToShow);
if (vm) {
params = params || [];
vm.beforeShow?.(...params);
vm.modalVisible(true);
vm.onShow?.(...params);
}
},
@ -285,7 +266,7 @@ export const
});
const cross = new Crossroads();
cross.addRoute(/^([a-zA-Z0-9-]*)\/?(.*)$/, screenOnRoute);
cross.addRoute(/^([^/]*)\/?(.*)$/, screenOnRoute);
hasher.add(cross.parse.bind(cross));
hasher.init();

197
dev/Mime/Address.js Normal file
View file

@ -0,0 +1,197 @@
import { decodeEncodedWords } from 'Mime/Encoding';
/**
* Parses structured e-mail addresses from an address/mailbox(-list) field
* https://datatracker.ietf.org/doc/html/rfc2822#section-3.4
*
* Example:
*
* "Name <address@domain>"
*
* will be converted to
*
* [{name: "Name", email: "address@domain"}]
*
* @param {String} str Address field
* @return {Array} An array of address objects
*/
export function addressparser(str) {
str = (str || '').toString();
let
endOperator = '',
node = {
type: 'text',
value: ''
},
escaped = false,
address = [],
addresses = [];
const
/*
* Operator tokens and which tokens are expected to end the sequence
*/
OPERATORS = {
'"': '"',
'(': ')',
'<': '>',
',': '',
// Groups are ended by semicolons
':': ';',
// Semicolons are not a legal delimiter per the RFC2822 grammar other
// than for terminating a group, but they are also not valid for any
// other use in this context. Given that some mail clients have
// historically allowed the semicolon as a delimiter equivalent to the
// comma in their UI, it makes sense to treat them the same as a comma
// when used outside of a group.
';': ''
},
pushToken = token => {
token.value = (token.value || '').toString().trim();
token.value.length && address.push(token);
node = {
type: 'text',
value: ''
},
escaped = false;
},
pushAddress = () => {
if (address.length) {
address = _handleAddress(address);
if (address.length) {
addresses = addresses.concat(address);
}
}
address = [];
};
[...str].forEach(chr => {
if (!escaped && (chr === endOperator || (!endOperator && chr in OPERATORS))) {
pushToken(node);
if (',' === chr || ';' === chr) {
pushAddress();
} else {
endOperator = endOperator ? '' : OPERATORS[chr];
if ('<' === chr) {
node.type = 'email';
} else if ('(' === chr) {
node.type = 'comment';
} else if (':' === chr) {
node.type = 'group';
}
}
} else {
node.value += chr;
escaped = !escaped && '\\' === chr;
}
});
pushToken(node);
pushAddress();
return addresses;
}
/**
* Converts tokens for a single address into an address object
*
* @param {Array} tokens Tokens object
* @return {Object} Address object
*/
function _handleAddress(tokens) {
let
isGroup = false,
address = {},
addresses = [],
data = {
email: [],
comment: [],
group: [],
text: []
};
tokens.forEach(token => {
isGroup = isGroup || 'group' === token.type;
data[token.type].push(token.value);
});
// If there is no text but a comment, replace the two
if (!data.text.length && data.comment.length) {
data.text = data.comment;
data.comment = [];
}
if (isGroup) {
// http://tools.ietf.org/html/rfc2822#appendix-A.1.3
/*
addresses.push({
email: '',
name: data.text.join(' ').trim(),
group: addressparser(data.group.join(','))
// ,comment: data.comment.join(' ').trim()
});
*/
addresses = addresses.concat(addressparser(data.group.join(',')));
} else {
// If no address was found, try to detect one from regular text
if (!data.email.length && data.text.length) {
var i = data.text.length;
while (i--) {
if (data.text[i].match(/^[^@\s]+@[^@\s]+$/)) {
data.email = data.text.splice(i, 1);
break;
}
}
// still no address
if (!data.email.length) {
i = data.text.length;
while (i--) {
data.text[i] = data.text[i].replace(/\s*\b[^@\s]+@[^@\s]+\b\s*/, address => {
if (!data.email.length) {
data.email = [address.trim()];
return '';
}
return address.trim();
});
if (data.email.length) {
break;
}
}
}
}
// If there's still no text but a comment exists, replace the two
if (!data.text.length && data.comment.length) {
data.text = data.comment;
data.comment = [];
}
// Keep only the first address occurence, push others to regular text
if (data.email.length > 1) {
data.text = data.text.concat(data.email.splice(1));
}
address = {
// Join values with spaces
email: decodeEncodedWords(data.email.join(' ').trim()),
name: decodeEncodedWords(data.text.join(' ').trim())
// ,comment: data.comment.join(' ').trim()
};
if (address.email === address.name) {
if (address.email.includes('@')) {
address.name = '';
} else {
address.email = '';
}
}
// address.email = address.email.replace(/^[<]+(.*)[>]+$/g, '$1');
addresses.push(address);
}
return addresses;
}

35
dev/Mime/Encoding.js Normal file
View file

@ -0,0 +1,35 @@
const
QPDecodeParams = [/=([0-9A-F]{2})/g, (...args) => String.fromCharCode(parseInt(args[1], 16))];
export const
// https://datatracker.ietf.org/doc/html/rfc2045#section-6.8
BDecode = atob,
// unescape(encodeURIComponent()) makes the UTF-16 DOMString to an UTF-8 string
BEncode = data => btoa(unescape(encodeURIComponent(data))),
/* // Without deprecated 'unescape':
BEncode = data => btoa(encodeURIComponent(data).replace(
/%([0-9A-F]{2})/g, (match, p1) => String.fromCharCode('0x' + p1)
)),
*/
// https://datatracker.ietf.org/doc/html/rfc2045#section-6.7
QPDecode = data => data.replace(/=\r?\n/g, '').replace(...QPDecodeParams),
// https://datatracker.ietf.org/doc/html/rfc2047#section-4.1
// https://datatracker.ietf.org/doc/html/rfc2047#section-4.2
// encoded-word = "=?" charset "?" encoding "?" encoded-text "?="
decodeEncodedWords = data =>
data.replace(/=\?([^?]+)\?(B|Q)\?(.+?)\?=/g, (m, charset, encoding, text) =>
decodeText(charset, 'B' == encoding ? BDecode(text) : QPDecode(text))
)
,
decodeText = (charset, data) => {
try {
// https://developer.mozilla.org/en-US/docs/Web/API/Encoding_API/Encodings
return new TextDecoder(charset).decode(Uint8Array.from(data, c => c.charCodeAt(0)));
} catch (e) {
console.error({charset:charset,error:e});
}
};

View file

@ -1,16 +1,5 @@
const
// RFC2045
QPDecodeParams = [/=([0-9A-F]{2})/g, (...args) => String.fromCharCode(parseInt(args[1], 16))],
QPDecode = data => data.replace(/=\r?\n/g, '').replace(...QPDecodeParams),
decodeText = (charset, data) => {
try {
// https://developer.mozilla.org/en-US/docs/Web/API/Encoding_API/Encodings
return new TextDecoder(charset).decode(Uint8Array.from(data, c => c.charCodeAt(0)));
} catch (e) {
console.error({charset:charset,error:e});
}
};
import { decodeEncodedWords, BDecode, BEncode, QPDecode, decodeText } from 'Mime/Encoding';
import { addressparser } from 'Mime/Address';
export function ParseMime(text)
{
@ -26,7 +15,52 @@ export function ParseMime(text)
this.bodyEnd = 0;
this.boundary = '';
this.bodyText = '';
this.headers = {};
// https://datatracker.ietf.org/doc/html/rfc2822#section-3.6
// https://datatracker.ietf.org/doc/html/rfc4021
this.headers = {
// Required
date = null,
from = [], // mailbox-list
// Optional
sender = [], // mailbox MUST occur with multi-address
'reply-to' = [], // address-list
to = [], // address-list
cc = [], // address-list
bcc = [], // address-list
'message-id' = '', // msg-id SHOULD be present
'in-reply-to' = '', // 1*msg-id SHOULD occur in some replies
references = '', // 1*msg-id SHOULD occur in some replies
subject = '', // unstructured
// Optional unlimited
comments = [], // unstructured
keywords = [], // phrase *("," phrase)
// https://datatracker.ietf.org/doc/html/rfc2822#section-3.6.6
'resent-date' = [],
'resent-from' = [],
'resent-sender' = [],
'resent-to' = [],
'resent-cc' = [],
'resent-bcc' = [],
'resent-msg-id' = [],
// https://datatracker.ietf.org/doc/html/rfc2822#section-3.6.7
trace = [],
'return-path' = '', // angle-addr
received = [],
// optional others outside RFC2822
'mime-version' = '', // RFC2045
'content-transfer-encoding' = '',
'content-type' = '',
'delivered-to' = [], // RFC9228 addr-spec
'authentication-results' = '', // dkim, spf, dmarc
'dkim-signature' = '',
'x-rspamd-queue-id' = '',
'x-rspamd-action' = '',
'x-spamd-bar' = '',
'x-rspamd-server' = '',
'x-spamd-result' = '',
'x-remote-address' = '',
// etc.
};
}
*/
@ -49,25 +83,25 @@ 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) {
body = atob(body.replace(/\r?\n/g, ''));
body = BDecode(body.replace(/\r?\n/g, ''));
}
return decodeText(charset, body);
}
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 {
if ('quoted-printable' == encoding) {
body = QPDecode(body);
}
body = btoa(body);
body = BEncode(body);
}
return 'data:' + this.headerValue('content-type') + ';base64,' + body;
}
@ -78,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;
@ -90,6 +124,9 @@ export function ParseMime(text)
}
}
// mailbox-list or address-list
const lists = ['from','reply-to','to','cc','bcc'];
const ParsePart = (mimePart, start_pos = 0, id = '') =>
{
let part = new MimePart,
@ -111,11 +148,19 @@ export function ParseMime(text)
[...header.matchAll(/;\s*([^;=]+)=\s*"?([^;"]+)"?/g)].forEach(param =>
params[param[1].trim().toLowerCase()] = param[2].trim()
);
// encoded-word = "=?" charset "?" encoding "?" encoded-text "?="
match[2] = match[2].trim().replace(/=\?([^?]+)\?(B|Q)\?(.+?)\?=/g, (m, charset, encoding, text) =>
decodeText(charset, 'B' == encoding ? atob(text) : QPDecode(text))
);
headers[match[1].trim().toLowerCase()] = {
let field = match[1].trim().toLowerCase();
if (lists.includes(field)) {
match[2] = addressparser(match[2]);
} else if ('keywords' === field) {
match[2] = match[2].split(',').forEach(entry => decodeEncodedWords(entry.trim()));
match[2] = (headers[field]?.value || []).concat(match[2]);
} else {
match[2] = decodeEncodedWords(match[2].trim());
if ('comments' === field) {
match[2] = (headers[field]?.value || []).push(match[2]);
}
}
headers[field] = {
value: match[2],
params: params
};
@ -130,7 +175,7 @@ export function ParseMime(text)
let boundary = headers['content-type']?.params.boundary;
if (boundary) {
part.boundary = boundary;
let regex = new RegExp('(?:^|\r?\n)--' + boundary + '(?:--)?(?:\r?\n|$)', 'g'),
let regex = new RegExp('(?:^|\r?\n)--' + RegExp.escape(boundary) + '(?:--)?(?:\r?\n|$)', 'g'),
body = mimePart.slice(head.length),
bodies = body.split(regex),
pos = part.bodyStart;

View file

@ -1,38 +1,43 @@
import { ParseMime } from 'Mime/Parser';
import { AttachmentModel } from 'Model/Attachment';
import { EmailModel } from 'Model/Email';
import { FileInfo } from 'Common/File';
import { BEGIN_PGP_MESSAGE } from 'Stores/User/Pgp';
import { EmailModel } from 'Model/Email';
/**
* @param string data
* @param MessageModel message
*/
export function MimeToMessage(data, message)
{
let signed;
const struct = ParseMime(data);
if (struct.headers) {
let html = struct.getByContentType('text/html');
let html = struct.getByContentType('text/html'),
subject = struct.headerValue('subject');
html = html ? html.body : '';
if (struct.headers.subject) {
message.subject(struct.headers.subject.value);
}
// Content-Type: ...; protected-headers="v1"
subject && message.subject(subject);
// EmailCollectionModel
['from','to'].forEach(name => {
if (struct.headers[name] && !message[name].length) {
let mail = new EmailModel;
mail.parse(struct.headers[name].value);
message[name].push(mail);
}
const items = message[name];
struct.headerValue(name)?.forEach(item => {
item = new EmailModel(item.email, item.name);
// Make them unique
if (item.email && item.name || !items.find(address => address.email == item.email)) {
items.push(item);
}
});
});
struct.forEach(part => {
let cd = part.header('content-disposition'),
cid = part.header('content-id'),
cId = part.header('content-id'),
type = part.header('content-type');
if (cid || cd) {
if (cId || cd) {
// if (cd && 'attachment' === cd.value) {
let attachment = new AttachmentModel;
attachment.mimeType = type.value;
@ -40,17 +45,15 @@ export function MimeToMessage(data, message)
attachment.fileNameExt = attachment.fileName.replace(/^.+(\.[a-z]+)$/, '$1');
attachment.fileType = FileInfo.getType('', type.value);
attachment.url = part.dataUrl;
attachment.friendlySize = FileInfo.friendlySize(part.body.length);
attachment.estimatedSize = part.body.length;
/*
attachment.isThumbnail = false;
attachment.contentLocation = '';
attachment.download = '';
attachment.folder = '';
attachment.uid = '';
attachment.mimeIndex = part.id;
*/
attachment.cid = cid ? cid.value : '';
if (cid && html) {
attachment.cId = cId ? cId.value : '';
if (cId && html) {
let cid = 'cid:' + attachment.contentId(),
found = html.includes(cid);
attachment.isInline(found);
@ -62,12 +65,28 @@ export function MimeToMessage(data, message)
} else {
message.attachments.push(attachment);
}
} else if ('multipart/signed' === type.value && 'application/pgp-signature' === type.params.protocol) {
signed = {
MicAlg: type.micalg,
BodyPart: part.parts[0],
SigPart: part.parts[1]
};
} else if ('multipart/signed' === type.value) {
let protocol = type.params.protocol;
if ('application/pgp-signature' === protocol) {
message.pgpSigned({
micAlg: type.micalg,
bodyPart: part.parts[0],
sigPart: part.parts[1]
});
} else if ('application/pkcs7-signature' === protocol.replace('x-')) {
message.smimeSigned({
micAlg: type.micalg,
bodyPart: part,
sigPart: part.parts[1], // For importing
detached: true
});
}
} else if ('application/pkcs7-mime' === type.value /*&& 'signed-data' === type.params['smime-type']=*/) {
message.smimeSigned({
micAlg: type.micalg,
bodyPart: part,
detached: false
});
}
});
@ -78,10 +97,7 @@ export function MimeToMessage(data, message)
message.plain(data);
}
if (!signed && message.plain().includes(BEGIN_PGP_MESSAGE)) {
signed = true;
if (message.plain().includes(BEGIN_PGP_MESSAGE)) {
message.pgpSigned(true);
}
message.pgpSigned(signed);
// TODO: Verify instantly?
}

View file

@ -5,7 +5,7 @@ export class AbstractCollectionModel extends Array
constructor() {
/*
if (new.target === AbstractCollectionModel) {
throw new Error("Can't instantiate AbstractCollectionModel!");
throw Error("Can't instantiate AbstractCollectionModel!");
}
*/
super();

View file

@ -1,6 +1,7 @@
import { AbstractModel } from 'Knoin/AbstractModel';
import { addObservablesTo } from 'External/ko';
import Remote from 'Remote/User/Fetch';
import { SettingsUserStore } from 'Stores/User/Settings';
export class AccountModel extends AbstractModel {
/**
@ -23,7 +24,12 @@ export class AccountModel extends AbstractModel {
});
// Load at random between 3 and 30 seconds
isAdditional && setTimeout(()=>this.fetchUnread(), (Math.ceil(Math.random() * 10)) * 3000);
SettingsUserStore.showUnreadCount() && isAdditional
&& setTimeout(()=>this.fetchUnread(), (Math.ceil(Math.random() * 10)) * 3000);
}
label() {
return this.name || IDN.toUnicode(this.email);
}
/**

View file

@ -1,6 +1,8 @@
import ko from 'ko';
import { FileInfo, FileType } from 'Common/File';
import { stopEvent, SettingsGet, SettingsCapa } from 'Common/Globals';
import { b64EncodeJSONSafe } from 'Common/Utils';
import {
attachmentDownload,
serverRequestRaw
@ -18,13 +20,12 @@ export class AttachmentModel extends AbstractModel {
this.checked = ko.observable(true);
this.mimeType = '';
// this.mimeTypeParams = '';
this.fileName = '';
this.fileNameExt = '';
this.fileType = FileType.Unknown;
this.isThumbnail = false;
this.cid = '';
this.cId = '';
this.contentLocation = '';
this.download = '';
this.folder = '';
this.uid = '';
this.url = '';
@ -52,7 +53,7 @@ export class AttachmentModel extends AbstractModel {
}
toggleChecked(self, event) {
event.stopPropagation();
stopEvent(event);
self.checked(!self.checked());
}
@ -61,7 +62,7 @@ export class AttachmentModel extends AbstractModel {
}
contentId() {
return this.cid.replace(/^<+|>+$/g, '');
return this.cId.replace(/^<+|>+$/g, '');
}
/**
@ -124,6 +125,20 @@ export class AttachmentModel extends AbstractModel {
);
}
get download() {
return b64EncodeJSONSafe(this.url ? {
fileName: this.fileName,
data: this.url.replace(/^.+,/, '')
} : {
folder: this.folder,
uid: this.uid,
mimeIndex: this.mimeIndex,
mimeType: this.mimeType,
fileName: this.fileName,
accountHash: SettingsGet('accountHash')
});
}
/**
* @returns {string}
*/
@ -142,7 +157,7 @@ export class AttachmentModel extends AbstractModel {
* @returns {boolean}
*/
hasThumbnail() {
return this.isThumbnail && !this.isLinked();
return SettingsCapa('AttachmentThumbnails') && this.isImage() && !this.isLinked();
}
/**
@ -151,7 +166,7 @@ export class AttachmentModel extends AbstractModel {
thumbnailStyle() {
return this.hasThumbnail()
? 'background:url(' + serverRequestRaw('ViewThumbnail', this.download) + ')'
: '';
: null;
}
/**

View file

@ -1,3 +1,4 @@
import { baseCollator } from 'Common/Translator';
import { AbstractCollectionModel } from 'Model/AbstractCollection';
import { AttachmentModel } from 'Model/Attachment';
@ -10,22 +11,32 @@ export class AttachmentCollectionModel extends AbstractCollectionModel
* @returns {AttachmentCollectionModel}
*/
static reviveFromJson(items) {
return super.reviveFromJson(items, attachment => AttachmentModel.reviveFromJson(attachment));
/*
const attachments = super.reviveFromJson(items, attachment => AttachmentModel.reviveFromJson(attachment));
let collator = baseCollator(true);
attachments.sort((a, b) => {
if (a.isInline()) {
if (!b.isInline()) {
return 1;
}
} else if (!b.isInline()) {
return -1;
}
return collator.compare(a.fileName, b.fileName);
});
/*
if (attachments) {
attachments.InlineCount = attachments.reduce((accumulator, a) => accumulator + (a.isInline ? 1 : 0), 0);
}
return attachments;
*/
return attachments;
}
/**
* @param {string} cid
* @param {string} cId
* @returns {*}
*/
findByCid(cid) {
cid = cid.replace(/^<+|>+$/g, '');
return this.find(item => cid === item.contentId());
findByCid(cId) {
cId = cId.replace(/^<+|>+$/g, '');
return this.find(item => cId === item.contentId());
}
}

View file

@ -10,16 +10,16 @@ export class ComposeAttachmentModel extends AbstractModel {
* @param {?number=} size = null
* @param {boolean=} isInline = false
* @param {boolean=} isLinked = false
* @param {string=} CID = ''
* @param {string=} cId = ''
* @param {string=} contentLocation = ''
*/
constructor(id, fileName, size = null, isInline = false, isLinked = false, CID = '', contentLocation = '') {
constructor(id, fileName, size = null, isInline = false, isLinked = false, cId = '', contentLocation = '') {
super();
this.id = id;
this.isInline = !!isInline;
this.isLinked = !!isLinked;
this.CID = CID;
this.cId = cId;
this.contentLocation = contentLocation;
this.fromMessage = false;

View file

@ -86,6 +86,7 @@ export class ContactModel extends AbstractModel {
focused: false,
selected: false,
checked: false,
sendToAll: true,
deleted: false,
readOnly: false,
@ -134,22 +135,6 @@ export class ContactModel extends AbstractModel {
});
}
/**
* @returns {Array|null}
*/
getNameAndEmailHelper() {
let name = (this.givenName() + ' ' + this.surName()).trim(),
email = this.email()[0]?.value();
/*
// this.jCard.getOne('fn')?.notEmpty() ||
this.jCard.parseFullName({set:true});
// let name = this.jCard.getOne('nickname'),
let name = this.jCard.getOne('fn'),
email = this.jCard.getOne('email');
*/
return email ? [email, name] : null;
}
/**
* @static
* @param {jCard} json
@ -209,19 +194,15 @@ export class ContactModel extends AbstractModel {
return contact;
}
/**
* @returns {string}
*/
generateUid() {
return '' + this.id;
}
addEmail() {
// home, work
this.email.push({
value: ko.observable('')
// type: prop.params.type
});
if (this.sendToAllDisplayStatus())
document.getElementById('send-to-all').style.display = 'block';
}
addTel() {
@ -264,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)
@ -304,7 +285,7 @@ export class ContactModel extends AbstractModel {
// jCard.set('rev', '2022-05-21T10:59:52Z')
return {
Uid: this.id,
uid: this.id,
jCard: JSON.stringify(jCard)
};
}
@ -318,4 +299,9 @@ export class ContactModel extends AbstractModel {
+ (this.checked() ? ' checked' : '')
+ (this.focused() ? ' focused' : '');
}
sendToAllDisplayStatus() {
return this.email.length > 1
}
}

View file

@ -4,271 +4,18 @@ import { AbstractModel } from 'Knoin/AbstractModel';
'use strict';
/**
* Parses structured e-mail addresses from an address field
*
* Example:
*
* "Name <address@domain>"
*
* will be converted to
*
* [{name: "Name", address: "address@domain"}]
*
* @param {String} str Address field
* @return {Array} An array of address objects
*/
function addressparser(str) {
var tokenizer = new Tokenizer(str);
var tokens = tokenizer.tokenize();
var addresses = [];
var address = [];
var parsedAddresses = [];
tokens.forEach(token => {
if (token.type === 'operator' && (token.value === ',' || token.value === ';')) {
address.length && addresses.push(address);
address = [];
} else {
address.push(token);
}
});
address.length && addresses.push(address);
addresses.forEach(address => {
address = _handleAddress(address);
if (address.length) {
parsedAddresses = parsedAddresses.concat(address);
}
});
return parsedAddresses;
}
/**
* Converts tokens for a single address into an address object
*
* @param {Array} tokens Tokens object
* @return {Object} Address object
*/
function _handleAddress(tokens) {
var isGroup = false;
var state = 'text';
var address = void 0;
var addresses = [];
var data = {
address: [],
comment: [],
group: [],
text: []
};
// Filter out <addresses>, (comments) and regular text
tokens.forEach(token => {
if (token.type === 'operator') {
switch (token.value) {
case '<':
state = 'address';
break;
case '(':
state = 'comment';
break;
case ':':
state = 'group';
isGroup = true;
break;
default:
state = 'text';
}
} else if (token.value) {
data[state].push(token.value);
}
});
// If there is no text but a comment, replace the two
if (!data.text.length && data.comment.length) {
data.text = data.comment;
data.comment = [];
}
if (isGroup) {
// http://tools.ietf.org/html/rfc2822#appendix-A.1.3
data.text = data.text.join(' ');
addresses.push({
name: data.text || address && address.name,
group: data.group.length ? addressparser(data.group.join(',')) : []
});
} else {
// If no address was found, try to detect one from regular text
if (!data.address.length && data.text.length) {
var i = data.text.length;
while (i--) {
if (data.text[i].match(/^[^@\s]+@[^@\s]+$/)) {
data.address = data.text.splice(i, 1);
break;
}
}
// still no address
if (!data.address.length) {
i = data.text.length;
while (i--) {
data.text[i] = data.text[i].replace(/\s*\b[^@\s]+@[^@\s]+\b\s*/, address => {
if (!data.address.length) {
data.address = [address.trim()];
return '';
}
return address.trim();
});
if (data.address.length) {
break;
}
}
}
}
// If there's still is no text but a comment exixts, replace the two
if (!data.text.length && data.comment.length) {
data.text = data.comment;
data.comment = [];
}
// Keep only the first address occurence, push others to regular text
if (data.address.length > 1) {
data.text = data.text.concat(data.address.splice(1));
}
// Join values with spaces
data.text = data.text.join(' ');
data.address = data.address.join(' ');
if (!data.address && isGroup) {
return [];
}
address = {
address: data.address || data.text || '',
name: data.text || data.address || ''
};
if (address.address === address.name) {
if ((address.address || '').match(/@/)) {
address.name = '';
} else {
address.address = '';
}
}
addresses.push(address);
}
return addresses;
}
/*
* Operator tokens and which tokens are expected to end the sequence
*/
var OPERATORS = {
'"': '"',
'(': ')',
'<': '>',
',': '',
// Groups are ended by semicolons
':': ';',
// Semicolons are not a legal delimiter per the RFC2822 grammar other
// than for terminating a group, but they are also not valid for any
// other use in this context. Given that some mail clients have
// historically allowed the semicolon as a delimiter equivalent to the
// comma in their UI, it makes sense to treat them the same as a comma
// when used outside of a group.
';': ''
};
class Tokenizer
{
constructor(str) {
this.str = (str || '').toString();
this.operatorCurrent = '';
this.operatorExpecting = '';
this.node = null;
this.escaped = false;
this.list = [];
}
tokenize() {
var list = [];
[...this.str].forEach(c => this.checkChar(c));
this.list.forEach(node => {
node.value = (node.value || '').toString().trim();
node.value && list.push(node);
});
return list;
}
checkChar(chr) {
if ((chr in OPERATORS || chr === '\\') && this.escaped) {
this.escaped = false;
} else if (this.operatorExpecting && chr === this.operatorExpecting) {
this.node = {
type: 'operator',
value: chr
};
this.list.push(this.node);
this.node = null;
this.operatorExpecting = '';
this.escaped = false;
return;
} else if (!this.operatorExpecting && chr in OPERATORS) {
this.node = {
type: 'operator',
value: chr
};
this.list.push(this.node);
this.node = null;
this.operatorExpecting = OPERATORS[chr];
this.escaped = false;
return;
}
if (!this.escaped && chr === '\\') {
this.escaped = true;
return;
}
if (!this.node) {
this.node = {
type: 'text',
value: ''
};
this.list.push(this.node);
}
if (this.escaped && chr !== '\\') {
this.node.value += '\\';
}
this.node.value += chr;
this.escaped = false;
}
}
export class EmailModel extends AbstractModel {
/**
* @param {string=} email = ''
* @param {string=} name = ''
* @param {string=} dkimStatus = 'none'
* @param {string=} dkimValue = ''
*/
constructor(email = '', name = '', dkimStatus = 'none', dkimValue = '') {
constructor(email, name, dkimStatus = 'none') {
super();
this.email = email;
this.name = name;
this.email = email || '';
this.name = name || '';
this.dkimStatus = dkimStatus;
this.dkimValue = dkimValue;
this.clearDuplicateName();
this.cleanup();
}
/**
@ -278,61 +25,34 @@ export class EmailModel extends AbstractModel {
*/
static reviveFromJson(json) {
const email = super.reviveFromJson(json);
email?.clearDuplicateName();
return email;
}
/**
* @returns {void}
*/
clear() {
this.email = '';
this.name = '';
this.dkimStatus = 'none';
this.dkimValue = '';
email?.cleanup();
return email?.valid() ? email : null;
}
/**
* @returns {boolean}
*/
validate() {
valid() {
return this.name || this.email;
}
/**
* @param {boolean} withoutName = false
* @returns {string}
*/
hash(withoutName = false) {
return '#' + (withoutName ? '' : this.name) + '#' + this.email + '#';
}
/**
* @returns {void}
*/
clearDuplicateName() {
cleanup() {
if (this.name === this.email) {
this.name = '';
}
}
/**
* @param {string} query
* @returns {boolean}
*/
search(query) {
return (this.name + ' ' + this.email).toLowerCase().includes(query.toLowerCase());
}
/**
* @param {boolean} friendlyView = false
* @param {boolean} wrapWithLink = false
* @returns {string}
*/
toLine(friendlyView, wrapWithLink) {
let result = this.email,
name = this.name,
let name = this.name,
result = this.email,
toLink = text =>
'<a href="mailto:'
+ encodeHtml(result) + (name ? '?to=' + encodeURIComponent('"' + name + '" <' + result + '>') : '')
@ -351,47 +71,11 @@ export class EmailModel extends AbstractModel {
result = toLink();
}
}
return result;
return result || name;
}
static splitEmailLine(line) {
const result = [];
let exists = false;
addressparser(line).forEach(item => {
const address = item.address
? new EmailModel(item.address.replace(/^[<]+(.*)[>]+$/g, '$1'), item.name || '')
: null;
if (address?.email) {
exists = true;
}
result.push(address ? address.toLine() : item.name);
});
return exists ? result : null;
}
static parseEmailLine(line) {
return addressparser(line).map(item =>
item.address ? new EmailModel(item.address.replace(/^[<]+(.*)[>]+$/g, '$1'), item.name || '') : null
).filter(v => v);
}
/**
* @param {string} emailAddress
* @returns {boolean}
*/
parse(emailAddress) {
emailAddress = emailAddress.trim();
if (emailAddress) {
const result = addressparser(emailAddress);
if (result.length) {
this.name = result[0].name || '';
this.email = result[0].address || '';
this.clearDuplicateName();
return true;
}
}
return false;
get domain() {
return this.email.replace(/^.+@([^@]+)$/, '$1');
// return this.email.slice(this.email.lastIndexOf('@'))
}
}

View file

@ -1,5 +1,7 @@
import { AbstractCollectionModel } from 'Model/AbstractCollection';
import { EmailModel } from 'Model/Email';
import { forEachObjectValue } from 'Common/Utils';
import { addressparser } from 'Mime/Address';
'use strict';
@ -13,6 +15,16 @@ export class EmailCollectionModel extends AbstractCollectionModel
return super.reviveFromJson(items, email => EmailModel.reviveFromJson(email));
}
/**
* @param {string} text
* @returns {EmailCollectionModel}
*/
static fromString(str) {
let list = new this();
list.fromString(str);
return list;
}
/**
* @param {boolean=} friendlyView = false
* @param {boolean=} wrapWithLink = false
@ -21,4 +33,43 @@ export class EmailCollectionModel extends AbstractCollectionModel
toString(friendlyView, wrapWithLink) {
return this.map(email => email.toLine(friendlyView, wrapWithLink)).join(', ');
}
/**
* @param {string} text
*/
fromString(str) {
if (str) {
let items = {}, key;
addressparser(str).forEach(item => {
item = new EmailModel(item.email, item.name);
// Make them unique
key = item.email || item.name;
if (key && (item.name || !items[key])) {
items[key] = item;
}
});
forEachObjectValue(items, item => this.push(item));
}
}
/**
* @param {array} [{name: "Name", email: "address@domain"}]
*/
/*
static fromArray(addresses) {
let list = new this();
list.fromArray(addresses);
return list;
}
fromArray(addresses) {
addresses.forEach(item => {
item = new EmailModel(item.email, item.name);
// Make them unique
if (item.email && item.name || !this.find(address => address.email == item.email)) {
this.push(item);
}
});
}
*/
}

View file

@ -3,7 +3,7 @@ import { AbstractCollectionModel } from 'Model/AbstractCollection';
import { UNUSED_OPTION_VALUE } from 'Common/Consts';
import { isArray, getKeyByValue, forEachObjectEntry, b64EncodeJSONSafe } from 'Common/Utils';
import { ClientSideKeyNameExpandedFolders, FolderType, FolderMetadataKeys } from 'Common/EnumsUser';
import { getFolderFromCacheList, setFolder, setFolderInboxName } from 'Common/Cache';
import { clearCache, getFolderFromCacheList, setFolder, setFolderInboxName } from 'Common/Cache';
import { Settings, SettingsGet, fireEvent } from 'Common/Globals';
import * as Local from 'Storage/Client';
@ -18,14 +18,21 @@ import { i18n, translateTrigger } from 'Common/Translator';
import { AbstractModel } from 'Knoin/AbstractModel';
import { koComputable, addObservablesTo } from 'External/ko';
import { /*koComputable,*/ addObservablesTo } from 'External/ko';
//import { mailBox } from 'Common/Links';
import { mailBox } from 'Common/Links';
import Remote from 'Remote/User/Fetch';
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()),
// isPosNumeric = value => null != value && /^[0-9]*$/.test(value.toString()),
normalizeFolder = sFolderFullName => ('' === sFolderFullName
|| UNUSED_OPTION_VALUE === sFolderFullName
@ -91,12 +98,13 @@ export const
Remote.abort('Folders')
.post('Folders', FolderUserStore.foldersLoading)
.then(data => {
clearCache();
FolderCollectionModel.reviveFromJson(data.Result)?.storeIt();
fCallback?.(true);
// 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
@ -109,6 +117,8 @@ export class FolderCollectionModel extends AbstractCollectionModel
this.namespace;
this.optimized
this.capabilities
this.allow; // allow adding
// this.exist;
}
*/
@ -124,11 +134,11 @@ export class FolderCollectionModel extends AbstractCollectionModel
);
const result = super.reviveFromJson(object, oFolder => {
let oCacheFolder = getFolderFromCacheList(oFolder.FullName);
let oCacheFolder = getFolderFromCacheList(oFolder.fullName);
if (oCacheFolder) {
// oCacheFolder.revivePropertiesFromJson(oFolder);
if (oFolder.Hash) {
oCacheFolder.hash = oFolder.Hash;
if (oFolder.etag) {
oCacheFolder.etag = oFolder.etag;
}
if (null != oFolder.totalEmails) {
oCacheFolder.totalEmails(oFolder.totalEmails);
@ -167,32 +177,32 @@ export class FolderCollectionModel extends AbstractCollectionModel
break;
}
// Flags
if (oFolder.Flags.includes('\\sentmail')) {
if (oFolder.attributes.includes('\\sentmail')) {
role = 'sent';
}
if (oFolder.Flags.includes('\\spam')) {
if (oFolder.attributes.includes('\\spam')) {
role = 'junk';
}
if (oFolder.Flags.includes('\\bin')) {
if (oFolder.attributes.includes('\\bin')) {
role = 'trash';
}
if (oFolder.Flags.includes('\\important')) {
if (oFolder.attributes.includes('\\important')) {
role = 'important';
}
if (oFolder.Flags.includes('\\starred')) {
if (oFolder.attributes.includes('\\starred')) {
role = 'flagged';
}
if (oFolder.Flags.includes('\\all') || oFolder.Flags.includes('\\allmail')) {
if (oFolder.attributes.includes('\\all') || oFolder.flags.includes('\\allmail')) {
role = 'all';
}
}
*/
if (role) {
role = role[0].toUpperCase() + role.slice(1);
SystemFolders[role] || (SystemFolders[role] = oFolder.FullName);
SystemFolders[role] || (SystemFolders[role] = oFolder.fullName);
}
oCacheFolder.type(FolderType[getKeyByValue(SystemFolders, oFolder.FullName)] || 0);
oCacheFolder.type(FolderType[getKeyByValue(SystemFolders, oFolder.fullName)] || 0);
oCacheFolder.collapsed(!expandedFolders
|| !isArray(expandedFolders)
@ -221,14 +231,13 @@ export class FolderCollectionModel extends AbstractCollectionModel
name = parents.pop(),
pfolder = getFolderFromCacheList(parentName);
if (!pfolder) {
console.log('Create nonexistent folder ' + parentName);
pfolder = FolderModel.reviveFromJson({
'@Object': 'Object/Folder',
Name: name,
FullName: parentName,
Delimiter: delimiter,
Exists: false,
isSubscribed: false,
Flags: ['\\nonexistent']
name: name,
fullName: parentName,
delimiter: delimiter,
attributes: ['\\nonexistent']
});
setFolder(pfolder);
result.splice(i, 0, pfolder);
@ -251,6 +260,10 @@ export class FolderCollectionModel extends AbstractCollectionModel
return result;
}
visible() {
return this.filter(folder => folder.visible());
}
storeIt() {
FolderUserStore.displaySpecSetting(Settings.app('folderSpecLimit') < this.CountRec);
@ -270,11 +283,9 @@ export class FolderCollectionModel extends AbstractCollectionModel
FolderUserStore.namespace = this.namespace;
// 'THREAD=REFS', 'THREAD=REFERENCES', 'THREAD=ORDEREDSUBJECT'
AppUserStore.threadsAllowed(!!(
Settings.app('useImapThread') && this.capabilities.some(capa => capa.startsWith('THREAD='))
));
AppUserStore.threadsAllowed(!!this.capabilities.some(capa => capa.startsWith('THREAD=')));
// FolderUserStore.folderListOptimized(!!this.optimized);
// FolderUserStore.optimized(!!this.optimized);
FolderUserStore.quotaUsage(this.quotaUsage);
FolderUserStore.quotaLimit(this.quotaLimit);
FolderUserStore.capabilities(this.capabilities);
@ -295,6 +306,7 @@ export class FolderModel extends AbstractModel {
super();
this.fullName = '';
this.parentName = '';
this.delimiter = '';
this.deep = 0;
this.expires = 0;
@ -302,9 +314,10 @@ export class FolderModel extends AbstractModel {
this.exists = true;
this.hash = '';
// this.id = null;
this.uidNext = null;
this.etag = '';
this.id = 0;
this.uidNext = 0;
this.size = 0;
addObservablesTo(this, {
name: '',
@ -314,16 +327,14 @@ export class FolderModel extends AbstractModel {
focused: false,
selected: false,
editing: false,
isSubscribed: true,
checkable: false, // Check for new messages
askDelete: false,
nameForEdit: '',
errorMsg: '',
totalEmailsValue: 0,
unreadEmailsValue: 0,
totalEmails: 0,
unreadEmails: 0,
kolabType: null,
@ -332,17 +343,19 @@ export class FolderModel extends AbstractModel {
tagsAllowed: false
});
this.flags = ko.observableArray();
this.attributes = ko.observableArray();
// For messages
this.permanentFlags = ko.observableArray();
this.addSubscribables({
kolabType: sValue => this.metadata[FolderMetadataKeys.KolabFolderType] = sValue,
permanentFlags: aValue => this.tagsAllowed(aValue.includes('\\*'))
permanentFlags: aValue => this.tagsAllowed(aValue.includes('\\*')),
unreadEmails: unread => FolderType.Inbox === this.type() && fireEvent('mailbox.inbox-unread-count', unread)
});
this.subFolders = ko.observableArray(new FolderCollectionModel);
this.actionBlink = ko.observable(false).extend({ falseTimeout: 1000 });
/*
this.totalEmails = koComputable({
read: this.totalEmailsValue,
write: iValue =>
@ -356,20 +369,132 @@ export class FolderModel extends AbstractModel {
isPosNumeric(value) ? this.unreadEmailsValue(value) : this.unreadEmailsValue.valueHasMutated()
})
.extend({ notify: 'always' });
/*
https://www.rfc-editor.org/rfc/rfc8621.html#section-2
"myRights": {
"mayAddItems": true,
"mayRename": false,
"maySubmit": true,
"mayDelete": false,
"maySetKeywords": true,
"mayRemoveItems": true,
"mayCreateChild": true,
"maySetSeen": true,
"mayReadItems": true
},
*/
/*
// https://www.rfc-editor.org/rfc/rfc8621.html#section-2
this.myRights = {
'mayAddItems': true,
'mayCreateChild': true,
'mayDelete': true,
'mayReadItems': true,
'mayRemoveItems': true,
'mayRename': true,
'maySetKeywords': true,
'maySetSeen': true,
'maySubmit': true
};
*/
this.addComputables({
isInbox: () => FolderType.Inbox === this.type(),
isFlagged: () => FolderUserStore.currentFolder() === this
&& MessagelistUserStore.listSearch().includes('flagged'),
// isSubscribed: () => this.attributes().includes('\\subscribed'),
hasVisibleSubfolders: () => !!this.subFolders().find(folder => folder.visible()),
visibleSubfolders: () => this.subFolders().visible(),
hasSubscriptions: () => this.isSubscribed() | !!this.subFolders().find(
oFolder => {
const subscribed = oFolder.hasSubscriptions();
return !oFolder.isSystemFolder() && subscribed;
}
),
isSystemFolder: () => this.type()
| (FolderUserStore.allowKolab() && !!this.kolabType() & !SettingsUserStore.unhideKolabFolders()),
canBeSelected: () => this.selectable() && !this.isSystemFolder(),
canBeDeleted: () => this.canBeSelected() && this.exists,
canBeSubscribed: () => this.selectable()
&& !(this.isSystemFolder() | !SettingsUserStore.hideUnsubscribed()),
optionalTags: () => this.permanentFlags.filter(isAllowedKeyword),
/**
* Folder is visible when:
* - hasVisibleSubfolders()
* Or when all below conditions are true:
* - selectable()
* - isSubscribed() OR hideUnsubscribed = false
* - 0 == type()
* - not kolabType()
*/
visible: () => {
const selectable = this.canBeSelected(),
name = this.name(),
filter = foldersFilter(),
visible = (this.isSubscribed() | !SettingsUserStore.hideUnsubscribed())
&& selectable
&& (!filter || name.toLowerCase().includes(filter.toLowerCase()));
return this.hasVisibleSubfolders() | visible;
},
unreadCount: () => this.unreadEmails() || null,
/*
{
// TODO: make this optional in Settings
// https://github.com/the-djmaze/snappymail/issues/457
// https://github.com/the-djmaze/snappymail/issues/567
const
unread = this.unreadEmails(),
type = this.type();
// return ((!this.isSystemFolder() || type == FolderType.Inbox) && unread) ? unread : null;
},
*/
localName: () => {
let name = this.name();
if (this.isSystemFolder()) {
translateTrigger();
name = getSystemFolderName(this.type(), name);
}
return name;
},
nameInfo: () => {
if (this.isSystemFolder()) {
translateTrigger();
let suffix = getSystemFolderName(this.type(), getKolabFolderName(this.kolabType()));
if (this.name() !== suffix && 'inbox' !== suffix.toLowerCase()) {
return ' (' + suffix + ')';
}
}
return '';
},
friendlySize: () => FileInfo.friendlySize(this.size),
detailedName: () => this.name() + ' ' + this.nameInfo(),
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)
});
}
edit() {
showScreenPopup(FolderPopupView, [this]);
}
/**
@ -389,6 +514,7 @@ export class FolderModel extends AbstractModel {
const folder = super.reviveFromJson(json);
if (folder) {
const path = folder.fullName.split(folder.delimiter),
attr = name => folder.attributes.includes(name),
type = (folder.metadata[FolderMetadataKeys.KolabFolderType]
|| folder.metadata[FolderMetadataKeys.KolabFolderTypeShared]
|| ''
@ -398,105 +524,13 @@ export class FolderModel extends AbstractModel {
path.pop();
folder.parentName = path.join(folder.delimiter);
folder.isSubscribed(attr('\\subscribed'));
folder.exists = !attr('\\nonexistent');
folder.subFolders.allow = !attr('\\noinferiors');
// folder.subFolders.exist = attr('\\haschildren') || !attr('\\hasnochildren');
folder.selectable(folder.exists && !attr('\\noselect'));
type && 'mail' != type && folder.kolabType(type);
folder.addComputables({
isInbox: () => FolderType.Inbox === folder.type(),
isFlagged: () => FolderUserStore.currentFolder() === folder
&& MessagelistUserStore.listSearch().includes('flagged'),
hasVisibleSubfolders: () => !!folder.subFolders().find(folder => folder.visible()),
hasSubscriptions: () => folder.isSubscribed() | !!folder.subFolders().find(
oFolder => {
const subscribed = oFolder.hasSubscriptions();
return !oFolder.isSystemFolder() && subscribed;
}
),
canBeEdited: () => !folder.type() && folder.exists/* && folder.selectable()*/,
isSystemFolder: () => folder.type()
| (FolderUserStore.allowKolab() && !!folder.kolabType() & !SettingsUserStore.unhideKolabFolders()),
canBeSelected: () => folder.selectable() && !folder.isSystemFolder(),
canBeDeleted: () => folder.canBeSelected() && folder.exists,
canBeSubscribed: () => folder.selectable()
&& !(folder.isSystemFolder() | !SettingsUserStore.hideUnsubscribed()),
/**
* Folder is visible when:
* - hasVisibleSubfolders()
* Or when all below conditions are true:
* - selectable()
* - isSubscribed() OR hideUnsubscribed = false
* - 0 == type()
* - not kolabType()
*/
visible: () => {
const selectable = folder.canBeSelected(),
name = folder.name(),
filter = foldersFilter(),
visible = (folder.isSubscribed() | !SettingsUserStore.hideUnsubscribed())
&& selectable
&& (!filter || name.toLowerCase().includes(filter.toLowerCase()));
return folder.hasVisibleSubfolders() | visible;
},
printableUnreadCount: () => folder.unreadEmails() || null,
/*
{
// TODO: make this optional in Settings
// https://github.com/the-djmaze/snappymail/issues/457
// https://github.com/the-djmaze/snappymail/issues/567
const
unread = folder.unreadEmails(),
type = folder.type();
// return ((!folder.isSystemFolder() || type == FolderType.Inbox) && unread) ? unread : null;
},
*/
localName: () => {
let name = folder.name();
if (folder.isSystemFolder()) {
translateTrigger();
name = getSystemFolderName(folder.type(), name);
}
return name;
},
manageFolderSystemName: () => {
if (folder.isSystemFolder()) {
translateTrigger();
let suffix = getSystemFolderName(folder.type(), getKolabFolderName(folder.kolabType()));
if (folder.name() !== suffix && 'inbox' !== suffix.toLowerCase()) {
return '(' + suffix + ')';
}
}
return '';
},
hasSubscribedUnreadMessagesSubfolders: () =>
!!folder.subFolders().find(
folder => folder.printableUnreadCount() | folder.hasSubscribedUnreadMessagesSubfolders()
)
/*
!!folder.subFolders().filter(
folder => folder.printableUnreadCount() | folder.hasSubscribedUnreadMessagesSubfolders()
).length
*/
// ,href: () => folder.canBeSelected() && mailBox(folder.fullNameHash)
});
folder.addSubscribables({
editing: value => value && folder.nameForEdit(folder.name()),
unreadEmails: unread => FolderType.Inbox === folder.type() && fireEvent('mailbox.inbox-unread-count', unread)
});
}
return folder;
}

View file

@ -1,36 +1,52 @@
import { AbstractModel } from 'Knoin/AbstractModel';
import { addObservablesTo } from 'External/ko';
//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
*/
constructor(id, email) {
export class IdentityModel extends EmailModel /*AbstractModel*/ {
constructor() {
super();
addObservablesTo(this, {
id: id || '',
email: email,
name: '',
id: '',
label: '',
// email: '',
// name: '',
replyTo: '',
bcc: '',
sentFolder: '',
signature: '',
signatureInsertBefore: false,
askDelete: false
pgpSign: false,
pgpEncrypt: false,
smimeKey: '',
smimeCertificate: '',
smimeCertificateChain: '',
askDelete: false,
exists: false
});
addComputablesTo(this, {
smimeKeyEncrypted: () => this.smimeKey().includes('-----BEGIN ENCRYPTED PRIVATE KEY-----'),
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();
return name ? name + ' <' + email + '>' : email;
toString() {
const name = this.name,
email = this.email,
label = this.label();
return (name ? `${name} ` : '') + `<${email}>` + (label ? ` (${label})` : '');
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,9 +1,6 @@
import { AbstractCollectionModel } from 'Model/AbstractCollection';
import { MessageModel } from 'Model/Message';
import {
MessageFlagsCache
} from 'Common/Cache';
import { MessageUserStore } from 'Stores/User/Message';
'use strict';
@ -12,18 +9,16 @@ export class MessageCollectionModel extends AbstractCollectionModel
/*
constructor() {
super();
this.Filtered
this.Folder
this.FolderHash
this.FolderInfo
this.filtered
this.folder
this.totalEmails
this.unreadEmails
this.UidNext
this.ThreadUid
this.NewMessages
this.Offset
this.Limit
this.Search
this.totalThreads
this.threadUid
this.newMessages
this.offset
this.limit
this.search
this.limited
}
*/
@ -31,14 +26,17 @@ export class MessageCollectionModel extends AbstractCollectionModel
* @param {?Object} json
* @returns {MessageCollectionModel}
*/
static reviveFromJson(object, cached) {
static reviveFromJson(object/*, cached*/) {
let msg = MessageUserStore.message();
return super.reviveFromJson(object, message => {
message = MessageModel.reviveFromJson(message);
if (message) {
message.deleted(false);
cached ? MessageFlagsCache.initMessage(message) : MessageFlagsCache.store(message);
return message;
// If message is currently viewed, use that.
if (msg && msg.hash === message.hash) {
msg.revivePropertiesFromJson(message);
message = msg;
} else {
message = MessageModel.reviveFromJson(message);
}
return message;
});
}
}

13
dev/Model/MimeHeader.js Normal file
View file

@ -0,0 +1,13 @@
import ko from 'ko';
import { AbstractModel } from 'Knoin/AbstractModel';
export class MimeHeaderModel extends AbstractModel
{
constructor() {
super();
this.name = '';
this.value = '';
this.parameters = ko.observableArray();
}
}

View file

@ -0,0 +1,34 @@
//import { AbstractModel } from 'Knoin/AbstractModel';
export class MimeHeaderAutocryptModel/* extends AbstractModel*/
{
constructor(value) {
// super();
this.addr = '';
this.prefer_encrypt = 'nopreference', // nopreference or mutual
this.keydata = '';
if (value) {
value.split(';').forEach(entry => {
entry = entry.match(/^([^=]+)=(.*)$/);
const trim = str => (str || '').trim().replace(/^["']|["']+$/g, '');
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) {
result += 'prefer-encrypt=mutual; ';
}
return result + 'keydata=' + this.keydata.replace(/\n/g, '\n ');
}
pem() {
return '-----BEGIN PGP PUBLIC KEY BLOCK-----\n\n'
+ this.keydata
+ '\n-----END PGP PUBLIC KEY BLOCK-----';
}
}

View file

@ -0,0 +1,38 @@
import { AbstractCollectionModel } from 'Model/AbstractCollection';
import { MimeHeaderModel } from 'Model/MimeHeader';
'use strict';
export class MimeHeaderCollectionModel extends AbstractCollectionModel
{
/**
* @param {?Array} json
* @returns {MimeHeaderCollectionModel}
*/
static reviveFromJson(items) {
return super.reviveFromJson(items, header => MimeHeaderModel.reviveFromJson(header));
}
/**
* @param {string} name
* @returns {?MimeHeader}
*/
getByName(name)
{
name = name.toLowerCase();
return this.find(header => header.name.toLowerCase() === name);
}
valueByName(name)
{
const header = this.getByName(name);
return header ? header.value : '';
}
valuesByName(name)
{
name = name.toLowerCase();
return this.filter(header => header.name.toLowerCase() === name).map(header => header.value);
}
}

View file

@ -1,4 +1,4 @@
import { Notification } from 'Common/Enums';
import { Notifications } from 'Common/Enums';
import { isArray, pInt, pString } from 'Common/Utils';
import { serverRequest } from 'Common/Links';
import { getNotification } from 'Common/Translator';
@ -8,19 +8,18 @@ let iJsonErrorCount = 0;
const getURL = (add = '') => serverRequest('Json') + pString(add),
checkResponseError = data => {
const err = data ? data.ErrorCode : null;
if (Notification.InvalidToken === err) {
console.error(getNotification(err));
const err = data ? data.code : null;
if (Notifications.InvalidToken === err) {
console.error(getNotification(err) + ` (${data.messageAdditional})`);
// alert(getNotification(err));
rl.logoutReload();
setTimeout(rl.logoutReload, 5000);
} else if ([
Notification.AuthError,
Notification.ConnectionError,
Notification.DomainNotAllowed,
Notification.AccountNotAllowed,
Notification.MailServerError,
Notification.UnknownNotification,
Notification.UnknownError
Notifications.AuthError,
Notifications.ConnectionError,
Notifications.DomainNotAllowed,
Notifications.AccountNotAllowed,
Notifications.MailServerError,
Notifications.UnknownError
].includes(err)
) {
if (7 < ++iJsonErrorCount) {
@ -32,10 +31,11 @@ checkResponseError = data => {
oRequests = {},
abort = (sAction, sReason, bClearOnly) => {
if (oRequests[sAction]) {
bClearOnly || oRequests[sAction].abort(sReason || 'AbortError');
oRequests[sAction] = null;
delete oRequests[sAction];
let controller = oRequests[sAction];
oRequests[sAction] = null;
if (controller) {
clearTimeout(controller.timeoutId);
bClearOnly || controller.abort(new DOMException(sAction, sReason || 'AbortError'));
}
},
@ -48,15 +48,18 @@ fetchJSON = (action, sUrl, params, timeout, jsonCallback) => {
}
}
// Don't abort, read https://github.com/the-djmaze/snappymail/issues/487
// abort(action);
// abort(action, 0, 1);
const controller = new AbortController(),
signal = controller.signal;
oRequests[action] = controller;
// Currently there is no way to combine multiple signals, so AbortSignal.timeout() not possible
timeout && setTimeout(() => abort(action, 'TimeoutError'), timeout);
return rl.fetchJSON(sUrl, {signal: signal}, params).then(jsonCallback).catch(err => {
controller.timeoutId = timeout && setTimeout(() => abort(action, 'TimeoutError'), timeout);
return rl.fetchJSON(sUrl, {signal: signal}, params).then(data => {
abort(action, 0, 1);
return jsonCallback ? jsonCallback(data) : Promise.resolve(data);
}).catch(err => {
clearTimeout(controller.timeoutId);
err.aborted = signal.aborted;
err.reason = signal.reason;
return Promise.reject(err);
});
};
@ -65,14 +68,14 @@ class FetchError extends Error
{
constructor(code, message) {
super(message);
this.code = code || Notification.JsonFalse;
this.code = code || Notifications.JsonFalse;
}
}
export class AbstractFetchRemote
{
abort(sAction) {
abort(sAction);
abort(sAction, sReason) {
abort(sAction, sReason);
return this;
}
@ -128,19 +131,10 @@ export class AbstractFetchRemote
fetchJSON(sAction, getURL(sGetAdd),
sGetAdd ? null : (params || {}),
undefined === iTimeout ? 30000 : pInt(iTimeout),
data => {
let cached = false;
if (data?.Time) {
cached = pInt(data.Time) > Date.now() - start;
}
pInt(iTimeout ?? 30000),
async data => {
let iError = 0;
if (sAction && oRequests[sAction]) {
abort(sAction, 0, 1);
}
if (!iError && data) {
if (data) {
/*
if (sAction !== data.Action) {
console.log(sAction + ' !== ' + data.Action);
@ -150,35 +144,35 @@ export class AbstractFetchRemote
iJsonErrorCount = 0;
} else {
checkResponseError(data);
iError = data.ErrorCode || Notification.UnknownError
iError = data.code || Notifications.UnknownError
}
}
if (111 === iError && rl.app.ask && await rl.app.ask.cryptkey()) {
return this.request(sAction, fCallback, params, iTimeout, sGetAdd);
}
fCallback && fCallback(
iError,
data,
cached,
sAction,
params
/**
* Responses like "304 Not Modified" are returned as "200 OK"
* This is an attempt to detect if the request comes from cache.
* But when client has wrong date/time, it will fail.
*/
data?.epoch && data.epoch < Math.floor(start / 1000) - 60
);
}
)
.catch(err => {
console.error({fetchError:err});
fCallback && fCallback(
'TimeoutError' == err.reason ? 3 : (err.name == 'AbortError' ? 2 : 1),
'TimeoutError' == err.name ? 3 : (err.name == 'AbortError' ? 2 : 1),
err
);
});
}
/**
* @param {?Function} fCallback
*/
getPublicKey(fCallback) {
this.request('GetPublicKey', fCallback);
}
setTrigger(trigger, value) {
if (trigger) {
value = !!value;
@ -195,16 +189,20 @@ export class AbstractFetchRemote
post(action, fTrigger, params, timeOut) {
this.setTrigger(fTrigger, true);
return fetchJSON(action, getURL(), params || {}, pInt(timeOut, 30000),
data => {
async data => {
abort(action, 0, 1);
if (!data) {
return Promise.reject(new FetchError(Notification.JsonParse));
return Promise.reject(new FetchError(Notifications.JsonParse));
}
if (111 === data?.code && rl.app.ask && await rl.app.ask.cryptkey()) {
return this.post(action, fTrigger, params, timeOut);
}
/*
let isCached = false, type = '';
if (data?.Time) {
isCached = pInt(data.Time) > microtime() - start;
if (data?.epoch) {
isCached = data.epoch > microtime() - start;
}
// backward capability
switch (true) {
@ -224,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

@ -9,57 +9,11 @@ import { SUB_QUERY_PREFIX } from 'Common/Links';
import { AppUserStore } from 'Stores/User/App';
import { SettingsUserStore } from 'Stores/User/Settings';
import { FolderUserStore } from 'Stores/User/Folder';
import { AbstractFetchRemote } from 'Remote/AbstractFetch';
class RemoteUserFetch extends AbstractFetchRemote {
/**
* @param {Function} fCallback
* @param {object} params
* @param {boolean=} bSilent = false
*/
messageList(fCallback, params, bSilent = false) {
const
sFolderFullName = pString(params.Folder),
folder = getFolderFromCacheList(sFolderFullName),
folderHash = folder?.hash || '';
params = Object.assign({
Offset: 0,
Limit: SettingsUserStore.messagesPerPage(),
Search: '',
UidNext: folder?.uidNext || 0, // Used to check for new messages
Sort: FolderUserStore.sortMode(),
Hash: folderHash + SettingsGet('AccountHash')
}, params);
params.Folder = sFolderFullName;
if (AppUserStore.threadsAllowed() && SettingsUserStore.useThreads()) {
params.UseThreads = 1;
} else {
params.ThreadUid = 0;
}
let sGetAdd = '';
if (folderHash && (!params.Search || !params.Search.includes('is:'))) {
sGetAdd = 'MessageList/' +
SUB_QUERY_PREFIX +
'/' +
b64EncodeJSONSafe(params);
params = {};
}
bSilent || this.abort('MessageList');
this.request('MessageList',
fCallback,
params,
60000, // 60 seconds before aborting
sGetAdd
);
}
/**
* @param {?Function} fCallback
* @param {string} sFolderFullName
@ -82,7 +36,7 @@ class RemoteUserFetch extends AbstractFetchRemote {
sFolderFullName,
iUid,
AppUserStore.threadsAllowed() && SettingsUserStore.useThreads() ? 1 : 0,
SettingsGet('AccountHash')
SettingsGet('accountHash')
])
);
@ -110,16 +64,6 @@ class RemoteUserFetch extends AbstractFetchRemote {
[key]: value
});
}
/*
folderMove(sPrevFolderFullName, sNewFolderFullName, bSubscribe) {
return this.post('FolderMove', FolderUserStore.foldersRenaming, {
Folder: sPrevFolderFullName,
NewFolder: sNewFolderFullName,
Subscribe: bSubscribe ? 1 : 0
});
}
*/
}
export default new RemoteUserFetch();

View file

@ -2,7 +2,7 @@ import ko from 'ko';
import { pString } from 'Common/Utils';
import { settings } from 'Common/Links';
import { createElement, elementById } from 'Common/Globals';
import { createElement/*, elementById*/, fireEvent } from 'Common/Globals';
import { AbstractScreen } from 'Knoin/AbstractScreen';
import { i18nToNodes } from 'Common/Translator';
@ -25,35 +25,42 @@ export class AbstractSettingsScreen extends AbstractScreen {
let settingsScreen = null,
viewModelDom = null,
RoutedSettingsViewModel = VIEW_MODELS.find(
SettingsViewModel => subName === SettingsViewModel.__rlSettingsData.route
SettingsViewModel => subName === SettingsViewModel.route
);
if (RoutedSettingsViewModel) {
const vmPlace = elementById('rl-settings-subscreen');
if (RoutedSettingsViewModel.__vm) {
settingsScreen = RoutedSettingsViewModel.__vm;
// const vmPlace = elementById('V-SettingsPane') || elementById('V-AdminPane);
const vmPlace = this.viewModels[1].__vm.viewModelDom,
SettingsViewModelClass = RoutedSettingsViewModel.vmc;
if (SettingsViewModelClass.__vm) {
settingsScreen = SettingsViewModelClass.__vm;
viewModelDom = settingsScreen.viewModelDom;
} else if (vmPlace) {
viewModelDom = createElement('div',{
id: 'V-Settings-' + RoutedSettingsViewModel.name.replace(/(User|Admin)Settings/,''),
id: 'V-Settings-' + SettingsViewModelClass.name.replace(/(User|Admin)Settings/,''),
hidden: ''
})
vmPlace.append(viewModelDom);
settingsScreen = new RoutedSettingsViewModel();
settingsScreen = new SettingsViewModelClass();
settingsScreen.viewModelDom = viewModelDom;
settingsScreen.viewModelTemplateID = RoutedSettingsViewModel.template;
RoutedSettingsViewModel.__dom = viewModelDom;
RoutedSettingsViewModel.__vm = settingsScreen;
SettingsViewModelClass.__vm = settingsScreen;
fireEvent('rl-view-model.create', settingsScreen);
ko.applyBindingAccessorsToNode(
viewModelDom,
{
template: () => ({ name: RoutedSettingsViewModel.__rlSettingsData.template })
template: () => ({ name: RoutedSettingsViewModel.template })
},
settingsScreen
);
settingsScreen.onBuild?.(viewModelDom);
fireEvent('rl-view-model', settingsScreen);
} else {
console.log('Cannot find sub settings view model position: SettingsSubScreen');
}
@ -68,13 +75,13 @@ export class AbstractSettingsScreen extends AbstractScreen {
// show
settingsScreen.beforeShow?.();
i18nToNodes(settingsScreen.viewModelDom);
settingsScreen.viewModelDom.hidden = false;
i18nToNodes(viewModelDom);
viewModelDom.hidden = false;
settingsScreen.onShow?.();
this.menu.forEach(item => {
item.selected(
item.route === RoutedSettingsViewModel.__rlSettingsData.route
item.route === RoutedSettingsViewModel.route
);
});
@ -96,19 +103,21 @@ export class AbstractSettingsScreen extends AbstractScreen {
}
onBuild() {
VIEW_MODELS.forEach(SettingsViewModel => this.menu.push(SettingsViewModel.__rlSettingsData));
// TODO: issue on account switch
// When current domain has sieve but the new has not, or current has not and the new has
// SettingsViewModel.disabled() || this.menu.push()
VIEW_MODELS.forEach(SettingsViewModel => this.menu.push(SettingsViewModel));
}
routes() {
const DefaultViewModel = VIEW_MODELS.find(
SettingsViewModel => SettingsViewModel.__rlSettingsData.isDefault
SettingsViewModel => SettingsViewModel.isDefault
),
defaultRoute =
DefaultViewModel ? DefaultViewModel.__rlSettingsData.route : 'general',
defaultRoute = DefaultViewModel?.route || 'general',
rules = {
subname: /^(.*)$/,
normalize_: (rquest, vals) => {
vals.subname = null == vals.subname ? defaultRoute : pString(vals.subname);
vals.subname = pString(vals.subname ?? defaultRoute);
return [vals.subname];
}
};
@ -131,13 +140,12 @@ export class AbstractSettingsScreen extends AbstractScreen {
*/
export function settingsAddViewModel(SettingsViewModelClass, template, labelName, route, isDefault = false) {
let name = SettingsViewModelClass.name.replace(/(User|Admin)Settings/, '');
SettingsViewModelClass.__rlSettingsData = {
label: labelName || 'SETTINGS_LABELS/LABEL_' + name.toUpperCase() + '_NAME',
VIEW_MODELS.push({
vmc: SettingsViewModelClass,
label: labelName || 'SETTINGS_LABELS/' + name.toUpperCase(),
route: route || name.toLowerCase(),
selected: ko.observable(false),
template: template || SettingsViewModelClass.name,
isDefault: !!isDefault
};
VIEW_MODELS.push(SettingsViewModelClass);
});
}

View file

@ -1,17 +1,14 @@
import { Scope } from 'Common/Enums';
import { Layout, ClientSideKeyNameMessageListSize } from 'Common/EnumsUser';
import { doc, createElement, leftPanelDisabled, Settings, elementById } from 'Common/Globals';
import { ScopeMessageList } from 'Common/Enums';
import { doc, createElement, Settings } from 'Common/Globals';
import { pString, pInt } from 'Common/Utils';
import { setLayoutResizer, moveAction } from 'Common/UtilsUser';
import { getFolderFromCacheList, getFolderFullName, getFolderInboxName } from 'Common/Cache';
import { moveAction } from 'Common/UtilsUser';
import { getFolderFromHashMap, getFolderInboxName } from 'Common/Cache';
import { i18n, initOnStartOrLangChange } from 'Common/Translator';
import { SettingsUserStore } from 'Stores/User/Settings';
import { AppUserStore } from 'Stores/User/App';
import { AccountUserStore } from 'Stores/User/Account';
import { FolderUserStore } from 'Stores/User/Folder';
import { MessagelistUserStore } from 'Stores/User/Messagelist';
import { ThemeStore } from 'Stores/Theme';
import { SystemDropDownUserView } from 'View/User/SystemDropDown';
import { MailFolderList } from 'View/User/MailBox/FolderList';
@ -59,11 +56,8 @@ export class MailBoxUserScreen extends AbstractScreen {
*/
onShow() {
this.updateWindowTitle();
AppUserStore.focusedState('none');
AppUserStore.focusedState(Scope.MessageList);
ThemeStore.isMobile() && leftPanelDisabled(true);
AppUserStore.focusedState(ScopeMessageList);
}
/**
@ -73,7 +67,8 @@ export class MailBoxUserScreen extends AbstractScreen {
* @returns {void}
*/
onRoute(folderHash, page, search, messageUid) {
const folder = getFolderFromCacheList(getFolderFullName(folderHash.replace(/~([\d]+)$/, '')));
// Only works when FolderUserStore.folderList() is loaded
const folder = getFolderFromHashMap(folderHash.replace(/~([\d]+)$/, ''));
if (folder) {
FolderUserStore.currentFolder(folder);
MessagelistUserStore.page(1 > page ? 1 : page);
@ -113,26 +108,8 @@ export class MailBoxUserScreen extends AbstractScreen {
* @returns {void}
*/
onBuild() {
setTimeout(() => {
// initMailboxLayoutResizer
const top = elementById('V-MailMessageList'),
bottom = elementById('V-MailMessageView'),
fToggle = () => {
let layout = SettingsUserStore.layout();
setLayoutResizer(top, bottom, ClientSideKeyNameMessageListSize,
(ThemeStore.isMobile() || Layout.NoPreview === layout)
? 0
: (Layout.SidePreview === layout ? 'Width' : 'Height')
);
};
if (top && bottom) {
fToggle();
addEventListener('rl-layout', fToggle);
}
}, 1);
doc.addEventListener('click', event =>
event.target.closest('#rl-right') && moveAction(false)
event.target.closest('#rl-right') && moveAction(0)
);
}

View file

@ -1,11 +1,10 @@
import { Scope } from 'Common/Enums';
import { keyScope, leftPanelDisabled, SettingsCapa } from 'Common/Globals';
import { ScopeSettings } from 'Common/Enums';
import { keyScope, SettingsCapa } from 'Common/Globals';
import { runSettingsViewModelHooks } from 'Common/Plugins';
import { initOnStartOrLangChange, i18n } from 'Common/Translator';
import { AppUserStore } from 'Stores/User/App';
import { AccountUserStore } from 'Stores/User/Account';
import { ThemeStore } from 'Stores/Theme';
import { AbstractSettingsScreen, settingsAddViewModel } from 'Screen/AbstractSettings';
@ -23,7 +22,7 @@ import { SettingsPaneUserView } from 'View/User/Settings/Pane';
export class SettingsUserScreen extends AbstractSettingsScreen {
constructor() {
super([SystemDropDownUserView, SettingsMenuUserView, SettingsPaneUserView]);
super([SettingsMenuUserView, SettingsPaneUserView, SystemDropDownUserView]);
const views = [
UserSettingsGeneral
@ -37,6 +36,8 @@ export class SettingsUserScreen extends AbstractSettingsScreen {
views.push(UserSettingsAccounts);
}
// TODO: issue on account switch
// When current domain has sieve but the new has not, or current has not and the new has
if (SettingsCapa('Sieve')) {
views.push(UserSettingsFilters);
}
@ -66,8 +67,7 @@ export class SettingsUserScreen extends AbstractSettingsScreen {
onShow() {
this.setSettingsTitle();
keyScope(Scope.Settings);
ThemeStore.isMobile() && leftPanelDisabled(true);
keyScope(ScopeSettings);
}
setSettingsTitle() {

View file

@ -16,6 +16,10 @@ export class AdminSettingsAbout /*extends AbstractViewSettings*/ {
coreWarning: false,
coreVersion: '',
coreVersionCompare: -2,
php64: true,
load1: 0,
load5: 0,
load15: 0,
errorDesc: ''
});
this.coreChecking = ko.observable(false).extend({ throttle: 100 });
@ -51,18 +55,22 @@ export class AdminSettingsAbout /*extends AbstractViewSettings*/ {
}
onBuild() {
Remote.request('AdminPHPExtensions', (iError, data) => iError || this.phpextensions(data.Result));
// beforeShow() {
this.coreChecking(true);
Remote.request('AdminUpdateInfo', (iError, data) => {
Remote.request('AdminInfo', (iError, data) => {
this.coreChecking(false);
if (!iError && data?.Result) {
data = data?.Result;
if (!iError && data) {
this.load1(data.system.load?.[0]);
this.load5(data.system.load?.[1]);
this.load15(data.system.load?.[2]);
this.phpextensions(data.php);
this.coreReal(true);
this.coreUpdatable(!!data.Result.Updatable);
this.coreWarning(!!data.Result.Warning);
this.coreVersion(data.Result.Version || '');
this.coreVersionCompare(data.Result.VersionCompare);
this.coreUpdatable(!!data.core.updatable);
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);
@ -72,6 +80,10 @@ export class AdminSettingsAbout /*extends AbstractViewSettings*/ {
});
}
clearCache() {
Remote.request('AdminClearCache');
}
updateCoreData() {
if (!this.coreUpdating()) {
this.coreUpdating(true);

View file

@ -3,8 +3,8 @@ import { AbstractViewSettings } from 'Knoin/AbstractViews';
export class AdminSettingsBranding extends AbstractViewSettings {
constructor() {
super();
this.addSetting('Title');
this.addSetting('LoadingDescription');
this.addSetting('FaviconUrl');
this.addSetting('title');
this.addSetting('loadingDescription');
this.addSetting('faviconUrl');
}
}

View file

@ -2,12 +2,40 @@ import ko from 'ko';
import Remote from 'Remote/Admin/Fetch';
import { forEachObjectEntry } from 'Common/Utils';
import { SettingsAdmin } from 'Common/Globals';
import { LanguageStore } from 'Stores/Language';
import { ThemeStore } from 'Stores/Theme';
export class AdminSettingsConfig /*extends AbstractViewSettings*/ {
constructor() {
this.config = ko.observableArray();
this.search = ko.observable('');
this.saved = ko.observable(false).extend({ falseTimeout: 5000 });
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, 'tbody', tbody => {
let show = match(tbody.querySelector('th'));
if (show) {
qsa(tbody, '[hidden]', n => n.hidden = false);
} else {
qsa(tbody, 'tbody td:first-child', td => {
let hide = !match(td);
show = show || !hide;
// td.closest('tr').hidden = hide;
td.parentNode.hidden = hide;
});
}
tbody.hidden = !show;
});
} else {
qsa(this.viewModelDom, 'table [hidden]', n => n.hidden = false);
}
});
}
beforeShow() {
@ -28,13 +56,19 @@ export class AdminSettingsConfig /*extends AbstractViewSettings*/ {
items: []
};
forEachObjectEntry(items, (skey, item) => {
if ('language' === skey) {
item[2] = ('webmail' === key) ? LanguageStore.languages : SettingsAdmin('languages');
} else if ('theme' === skey) {
item[2] = ThemeStore.themes;
}
'admin_password' === skey ||
section.items.push({
key: `config[${key}][${skey}]`,
name: skey,
value: item[0],
type: getInputType(item[0], skey.includes('password')),
comment: item[1]
comment: item[1],
options: item[2]
});
});
cfg.push(section);

View file

@ -13,16 +13,22 @@ export class AdminSettingsContacts extends AbstractViewSettings {
super();
this.defaultOptionsAfterRender = defaultOptionsAfterRender;
this.addSetting('ContactsPdoDsn');
this.addSetting('ContactsPdoUser');
this.addSetting('ContactsPdoPassword');
this.addSetting('ContactsPdoType', () => {
this.addSetting('contactsPdoDsn');
this.addSetting('contactsPdoUser');
this.addSetting('contactsPdoPassword');
this.addSetting('contactsPdoType', () => {
this.testContactsSuccess(false);
this.testContactsError(false);
this.testContactsErrorMessage('');
});
this.addSettings(['ContactsEnable','ContactsSync']);
this.addSettings(['contactsEnable','contactsSync']);
this.addSetting('contactsMySQLSSLCA');
this.addSetting('contactsMySQLSSLVerify');
this.addSetting('contactsMySQLSSLCiphers');
this.addSetting('contactsSQLiteGlobal');
addObservablesTo(this, {
testing: false,
@ -31,6 +37,8 @@ export class AdminSettingsContacts extends AbstractViewSettings {
testContactsErrorMessage: ''
});
this.addSetting('contactsSuggestionsLimit');
const supportedTypes = SettingsGet('supportedPdoDrivers') || [],
types = [{
id:'sqlite',
@ -90,10 +98,14 @@ export class AdminSettingsContacts extends AbstractViewSettings {
this.testing(false);
}, {
ContactsPdoType: this.contactsPdoType(),
ContactsPdoDsn: this.contactsPdoDsn(),
ContactsPdoUser: this.contactsPdoUser(),
ContactsPdoPassword: this.contactsPdoPassword()
PdoType: this.contactsPdoType(),
PdoDsn: this.contactsPdoDsn(),
PdoUser: this.contactsPdoUser(),
PdoPassword: this.contactsPdoPassword(),
MySQLSSLCA: this.contactsMySQLSSLCA(),
MySQLSSLVerify: this.contactsMySQLSSLVerify(),
MySQLSSLCiphers: this.contactsMySQLSSLCiphers(),
SQLiteGlobal: this.contactsSQLiteGlobal()
}
);
}

View file

@ -16,17 +16,16 @@ export class AdminSettingsDomains /*extends AbstractViewSettings*/ {
}
testUsername() {
// TODO: find domain matching username
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');
}
},
{
username: this.username()
username: this.username
}
);
}
@ -42,15 +41,15 @@ export class AdminSettingsDomains /*extends AbstractViewSettings*/ {
deleteDomain(domain) {
DomainAdminStore.remove(domain);
Remote.request('AdminDomainDelete', DomainAdminStore.fetch, {
Name: domain.name
name: domain.name
});
}
disableDomain(domain) {
domain.disabled(!domain.disabled());
Remote.request('AdminDomainDisable', DomainAdminStore.fetch, {
Name: domain.name,
Disabled: domain.disabled() ? 1 : 0
name: domain.name,
disabled: domain.disabled() ? 1 : 0
});
}
@ -60,7 +59,7 @@ export class AdminSettingsDomains /*extends AbstractViewSettings*/ {
el && ko.dataFor(el) && Remote.request('AdminDomainLoad',
(iError, oData) => iError || showScreenPopup(DomainPopupView, [oData.Result]),
{
Name: ko.dataFor(el).name
name: ko.dataFor(el).name
}
);

View file

@ -1,13 +1,9 @@
import ko from 'ko';
import {
isArray
} from 'Common/Utils';
import { addObservablesTo, addSubscribablesTo, addComputablesTo } from 'External/ko';
import { SaveSettingStatus } from 'Common/Enums';
import { Settings, SettingsGet, SettingsCapa } from 'Common/Globals';
import { SettingsAdmin, SettingsGet, SettingsCapa } from 'Common/Globals';
import { translatorReload, convertLangName } from 'Common/Translator';
import { AbstractViewSettings } from 'Knoin/AbstractViews';
@ -24,20 +20,14 @@ export class AdminSettingsGeneral extends AbstractViewSettings {
super();
this.language = LanguageStore.language;
this.languages = LanguageStore.languages;
const aLanguagesAdmin = Settings.app('languagesAdmin');
this.languagesAdmin = ko.observableArray(isArray(aLanguagesAdmin) ? aLanguagesAdmin : []);
this.languageAdmin = ko.observable(SettingsGet('LanguageAdmin'));
this.languageAdmin = ko.observable(SettingsAdmin('language'));
this.theme = ThemeStore.theme;
this.themes = ThemeStore.themes;
this.addSettings(['AllowLanguagesOnSettings']);
this.addSettings(['allowLanguagesOnSettings']);
addObservablesTo(this, {
attachmentLimitTrigger: SaveSettingStatus.Idle,
themeTrigger: SaveSettingStatus.Idle,
capaThemes: SettingsCapa('Themes'),
capaUserBackground: SettingsCapa('UserBackground'),
capaAdditionalAccounts: SettingsCapa('AdditionalAccounts'),
@ -55,14 +45,14 @@ export class AdminSettingsGeneral extends AbstractViewSettings {
*/
this.attachmentLimit = ko
.observable(SettingsGet('AttachmentLimit') / (1024 * 1024))
.observable(SettingsGet('attachmentLimit') / (1024 * 1024))
.extend({ debounce: 500 });
this.addSetting('Language');
this.addSetting('AttachmentLimit');
this.addSetting('language');
this.addSetting('attachmentLimit');
this.addSetting('Theme', value => changeTheme(value, this.themeTrigger));
this.uploadData = SettingsGet('PhpUploadSizes');
this.uploadData = SettingsGet('phpUploadSizes');
this.uploadDataDesc =
(this.uploadData?.upload_max_filesize || this.uploadData?.post_max_size)
? [
@ -91,9 +81,9 @@ export class AdminSettingsGeneral extends AbstractViewSettings {
addSubscribablesTo(this, {
languageAdmin: value => {
this.languageAdminTrigger(SaveSettingStatus.Saving);
translatorReload(true, value)
translatorReload(value, 1)
.then(fReloadLanguageHelper(SaveSettingStatus.Success), fReloadLanguageHelper(SaveSettingStatus.Failed))
.then(() => Remote.saveSetting('LanguageAdmin', value));
.then(() => Remote.saveSetting('languageAdmin', value));
},
capaAdditionalAccounts: fSaveHelper('CapaAdditionalAccounts'),
@ -109,14 +99,18 @@ export class AdminSettingsGeneral extends AbstractViewSettings {
}
selectLanguage() {
showScreenPopup(LanguagesPopupView, [this.language, this.languages(), LanguageStore.userLanguage()]);
showScreenPopup(LanguagesPopupView, [
this.language,
LanguageStore.languages,
LanguageStore.userLanguage()
]);
}
selectLanguageAdmin() {
showScreenPopup(LanguagesPopupView, [
this.languageAdmin,
this.languagesAdmin(),
SettingsGet('UserLanguageAdmin')
SettingsAdmin('languages'),
SettingsAdmin('clientLanguage')
]);
}
}

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