mirror of
https://github.com/the-djmaze/snappymail.git
synced 2026-07-13 03:27:39 +03:00
commit
92bfa7bd75
1300 changed files with 67163 additions and 24559 deletions
|
|
@ -3,7 +3,7 @@ module.exports = {
|
||||||
// extends: ['eslint:recommended', 'plugin:prettier/recommended'],
|
// extends: ['eslint:recommended', 'plugin:prettier/recommended'],
|
||||||
extends: ['eslint:recommended'],
|
extends: ['eslint:recommended'],
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
ecmaVersion: 6,
|
ecmaVersion: 11,
|
||||||
sourceType: 'module'
|
sourceType: 'module'
|
||||||
},
|
},
|
||||||
env: {
|
env: {
|
||||||
|
|
@ -35,7 +35,9 @@ module.exports = {
|
||||||
// vendors/bootstrap/bootstrap.native.js
|
// vendors/bootstrap/bootstrap.native.js
|
||||||
'BSN': "readonly",
|
'BSN': "readonly",
|
||||||
// Mailvelope
|
// Mailvelope
|
||||||
'mailvelope': "readonly"
|
'mailvelope': "readonly",
|
||||||
|
// Punycode
|
||||||
|
'IDN': "readonly"
|
||||||
},
|
},
|
||||||
// http://eslint.org/docs/rules/
|
// http://eslint.org/docs/rules/
|
||||||
rules: {
|
rules: {
|
||||||
|
|
|
||||||
396
CHANGELOG.md
396
CHANGELOG.md
|
|
@ -1,3 +1,399 @@
|
||||||
|
## 2.36.2 – 2024-05-26
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- "copy to" action in menu's for
|
||||||
|
[#1559](https://github.com/the-djmaze/snappymail/issues/1559)
|
||||||
|
- Log signal info for
|
||||||
|
[#1569](https://github.com/the-djmaze/snappymail/issues/1569)
|
||||||
|
- OpenPGP.js automatically import backup keys from server
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Improved "remember me" cookie handling
|
||||||
|
- Update Basque by @Thadah
|
||||||
|
- Update Portuguese by @ner00
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- "Account already exists"
|
||||||
|
[#1561](https://github.com/the-djmaze/snappymail/issues/1561)
|
||||||
|
- Properly escape path separator in tar.php file list regex by @sevmonster
|
||||||
|
[#1562](https://github.com/the-djmaze/snappymail/pull/1562)
|
||||||
|
- Prevent mkdir() error
|
||||||
|
[#1565](https://github.com/the-djmaze/snappymail/issues/1565)
|
||||||
|
- SCRAM Exception when trying to log in to SMTP
|
||||||
|
[#1575](https://github.com/the-djmaze/snappymail/issues/1575)
|
||||||
|
- Error when redirected back to instance after Gmail OAuth
|
||||||
|
[#1580](https://github.com/the-djmaze/snappymail/issues/1580)
|
||||||
|
- Uncaught TypeError: hasPublicKeyForEmails(...).then is not a function
|
||||||
|
[#1589](https://github.com/the-djmaze/snappymail/issues/1589)
|
||||||
|
- Undefined variable $sFilename
|
||||||
|
- GPG/PGP exec() return false handling
|
||||||
|
|
||||||
|
### Nextcloud
|
||||||
|
- OIDC login active again by @avinash-0007
|
||||||
|
[#1572](https://github.com/the-djmaze/snappymail/pull/1572)
|
||||||
|
|
||||||
|
|
||||||
|
## 2.36.1 – 2024-04-23
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Autoconfig detect through DNS SRV (RFC 6186 & 8314) and disable MX
|
||||||
|
- Have I Been Pwned class to check breached passwords and email addresses
|
||||||
|
- Handle RFC 5987 in Content-Disposition header
|
||||||
|
- Ignore text/x-amp-html
|
||||||
|
- Show SMTP error to user
|
||||||
|
[#1521](https://github.com/the-djmaze/snappymail/issues/1521)
|
||||||
|
- OAuth2 for login using gmail (and others)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- logMask all AUTHENTICATE requests
|
||||||
|
- ErrorTip use white-space: pre
|
||||||
|
- Simplify LoginProcess handling
|
||||||
|
- ES2020 everywhere (require Safari 13.1)
|
||||||
|
- Modified Squire to be more in line with v2.2.8
|
||||||
|
- CSS set min-width for .attachmentParent and .flagParent to line them up
|
||||||
|
- cPanel use extension login-cpanel instead of login-remote
|
||||||
|
- Improved login credentials handling
|
||||||
|
- Speedup Knockout a bit
|
||||||
|
- Update Belarusian by @spoooyders
|
||||||
|
- Update Chinese by @mayswind
|
||||||
|
- Update French by @hguilbert
|
||||||
|
- Update Polish by @tinola
|
||||||
|
- Update Portuguese by @ner00
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Content encoding and type detection in JavaScript could fail due to case-sensitivity.
|
||||||
|
- Extensions set logger failed
|
||||||
|
- GnuPG check open_basedir and if shell_exec is disabled
|
||||||
|
[#1385](https://github.com/the-djmaze/snappymail/issues/1385)
|
||||||
|
[#1496](https://github.com/the-djmaze/snappymail/issues/1496)
|
||||||
|
[#1555](https://github.com/the-djmaze/snappymail/issues/1555)
|
||||||
|
- Hide pagination when search result has no messages
|
||||||
|
- Prevent mbstring error before setup.php
|
||||||
|
- Prevent MessagesPerPage Infinity
|
||||||
|
[#1540](https://github.com/the-djmaze/snappymail/issues/1540)
|
||||||
|
- Reseal CryptKey failed
|
||||||
|
[#1543](https://github.com/the-djmaze/snappymail/issues/1543)
|
||||||
|
|
||||||
|
### Nextcloud
|
||||||
|
- Add an occ command to set up the login settings
|
||||||
|
[#1552](https://github.com/the-djmaze/snappymail/issues/1552)
|
||||||
|
|
||||||
|
|
||||||
|
## 2.36.0 – 2024-03-18
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Allow setting the supported THREAD algorithm
|
||||||
|
- Icon to system folders
|
||||||
|
- Remove remembered password after 15 minutes of inactivity
|
||||||
|
[#1142](https://github.com/the-djmaze/snappymail/issues/1142)
|
||||||
|
- Swap background and text color for unreadable text on dark background
|
||||||
|
[#1486](https://github.com/the-djmaze/snappymail/issues/1486)
|
||||||
|
- Generate TOTP code at ?Admin -> Security
|
||||||
|
[#1501](https://github.com/the-djmaze/snappymail/issues/1501)
|
||||||
|
- Button to change S/MIME private key passphrase
|
||||||
|
[#1505](https://github.com/the-djmaze/snappymail/issues/1505)
|
||||||
|
- Belarusian by @spoooyders
|
||||||
|
[#1512](https://github.com/the-djmaze/snappymail/pull/1512)
|
||||||
|
- Log some domain idn_to_ascii issues
|
||||||
|
[#1513](https://github.com/the-djmaze/snappymail/pull/1513)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- On folder/mailbox rename, also rename all children instead of reloading all
|
||||||
|
- Seal MainAccount CryptKey and on error ask old login passphrase to reseal key.
|
||||||
|
- Moved cache drivers outside core into extensions
|
||||||
|
- Sieve always allow fileinto INBOX
|
||||||
|
[#1510](https://github.com/the-djmaze/snappymail/issues/1510)
|
||||||
|
- Moved application.ini `sieve_auth_plain_initial` to per domain config
|
||||||
|
- Languages use rfc5646, by using the shortest ISO 639 code by default
|
||||||
|
- Update French by @hguilbert
|
||||||
|
- Update Portuguese by @ner00
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- On folder/mailbox rename, the old fullName must be removed from cache
|
||||||
|
- On folder/mailbox rename, the checkable option was not renamed
|
||||||
|
- Sort accounts drag & drop
|
||||||
|
- S/MIME encrypted and opaque signed not visible
|
||||||
|
[#1450](https://github.com/the-djmaze/snappymail/issues/1450)
|
||||||
|
- Wrong last UID of thread
|
||||||
|
[#1507](https://github.com/the-djmaze/snappymail/issues/1507)
|
||||||
|
- Creation of dynamic property SnappyMail\DAV\Client::$HTTP
|
||||||
|
[#1509](https://github.com/the-djmaze/snappymail/issues/1509)
|
||||||
|
- "Download as ZIP" fails for messages
|
||||||
|
[#1514](https://github.com/the-djmaze/snappymail/issues/1514)
|
||||||
|
- SMTP "Authentication failed" when IMAP uses `shortLogin` and SMTP not
|
||||||
|
[#1517](https://github.com/the-djmaze/snappymail/issues/1517)
|
||||||
|
|
||||||
|
|
||||||
|
## 2.35.4 – 2024-03-16
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- \SnappyMail\IDN::toAscii()
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- OpenPGP.js to v5.11.1
|
||||||
|
- punycode.js lowercase domain names
|
||||||
|
- application.ini `login_lowercase` removed and now configurable per domain JSON `lowerLogin`
|
||||||
|
- Update Portuguese by @ner00
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Raise JS TypeEroor "toLowerCase" after update
|
||||||
|
[#1491](https://github.com/the-djmaze/snappymail/issues/1491)
|
||||||
|
- Call to undefined function shell_exec
|
||||||
|
[#1496](https://github.com/the-djmaze/snappymail/issues/1496)
|
||||||
|
- Download attachments as ZIP doesn't work for PGP encrypted mail
|
||||||
|
[#1499](https://github.com/the-djmaze/snappymail/issues/1499)
|
||||||
|
- Importing or downloading a PGP public key attachment from a PGP encrypted message doesn't work
|
||||||
|
[#1500](https://github.com/the-djmaze/snappymail/issues/1500)
|
||||||
|
- VCard PHP Notice: Undefined index: ENCODING
|
||||||
|
|
||||||
|
### Nextcloud
|
||||||
|
- Changed stored password handling
|
||||||
|
- Can't login from nextcloud with 2.35.3 bug Nextcloud
|
||||||
|
[#1490](https://github.com/the-djmaze/snappymail/issues/1490)
|
||||||
|
|
||||||
|
|
||||||
|
## 2.35.3 – 2024-03-12
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- GnuPG can be disabled
|
||||||
|
- Missing strings for localization inside identity popup (Cryptography > S/MIME)
|
||||||
|
[#1458](https://github.com/the-djmaze/snappymail/issues/1458)
|
||||||
|
- Automatically verify PGP and S/MIME signed messages
|
||||||
|
- TNEFDecoder for
|
||||||
|
[#1012](https://github.com/the-djmaze/snappymail/discussions/1012)
|
||||||
|
- RTF to HTML converter for
|
||||||
|
[#1012](https://github.com/the-djmaze/snappymail/discussions/1012)
|
||||||
|
- Polyfill for PHP ctype
|
||||||
|
[#1250](https://github.com/the-djmaze/snappymail/issues/1250)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- `new Error()` to `Error()`
|
||||||
|
- Reduce KnockoutJS footprint by removing unused code
|
||||||
|
- CSS reposition rainloopErrorTip location
|
||||||
|
- Improved error handling on PGP and S/MIME decrypt
|
||||||
|
- Improved OpenPGP.js import keys
|
||||||
|
- Use Identity S/MIME key and certificate from server instead of POST
|
||||||
|
- application.ini `[webmail]language_admin` to `[admin_panel]language`
|
||||||
|
- application.ini `[security]admin_panel_host` to `[admin_panel]host`
|
||||||
|
- application.ini `[security]admin_panel_key` to `[admin_panel]key`
|
||||||
|
- Drop deprecated Domain::SetConfig()
|
||||||
|
- Internationalized domain names are now handled as punycode
|
||||||
|
- Cacher->Get() can now return NULL
|
||||||
|
- Update French by @hguilbert
|
||||||
|
- Update Polish by @tinola
|
||||||
|
- Update Portuguese by @ner00
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Handling of Internationalized Domain Names in several areas
|
||||||
|
- Decrypt error message
|
||||||
|
- Stalwart ManageSieve Error 352 when getting Filters
|
||||||
|
[#1455](https://github.com/the-djmaze/snappymail/issues/1455)
|
||||||
|
- Nextcloud V25+ theme slightly broken
|
||||||
|
[#1463](https://github.com/the-djmaze/snappymail/issues/1463)
|
||||||
|
- PGP decryption fails with "Not armored text"
|
||||||
|
[#1462](https://github.com/the-djmaze/snappymail/issues/1462)
|
||||||
|
- AUTH_BASIC falling through as AUTH_BEARER; change AUTH_BEARER to a different value
|
||||||
|
[#1461](https://github.com/the-djmaze/snappymail/issues/1461)
|
||||||
|
- SetPassword expects \SnappyMail\SensitiveString
|
||||||
|
- Crash on importing corrupt OpenPGP keys
|
||||||
|
- Crash on old browsers instead of showing error
|
||||||
|
- Ignore popups on logoutReload()
|
||||||
|
[#1467](https://github.com/the-djmaze/snappymail/pull/1467)
|
||||||
|
- Custom SASLMechanisms fail in IMAP when the connection is secure
|
||||||
|
[#1484](https://github.com/the-djmaze/snappymail/pull/1484)
|
||||||
|
|
||||||
|
|
||||||
|
## 2.35.2 – 2024-02-27
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- GnuPG error handling
|
||||||
|
- Missing strings for localization inside identity popup (Cryptography > S/MIME)
|
||||||
|
[#1458](https://github.com/the-djmaze/snappymail/issues/1458)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Update Portuguese by @ner00
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Drop support for gnupg PECL extension as it fails with "no passphrase" issues
|
||||||
|
- Error 352 when getting Filters
|
||||||
|
[#1455](https://github.com/the-djmaze/snappymail/issues/1455)
|
||||||
|
|
||||||
|
### Nextcloud
|
||||||
|
- SetPassword(): Argument #1 must be of type SensitiveString, string given
|
||||||
|
[#1456](https://github.com/the-djmaze/snappymail/issues/1456)
|
||||||
|
|
||||||
|
|
||||||
|
## 2.35.1 – 2024-02-26
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Search functionality in Admin -> Config
|
||||||
|
- Cache S/MIME passphrases when "remember" is checked
|
||||||
|
- Import S/MIME certificate popup
|
||||||
|
- pre-verify S/MIME opaque signed messages so we have a body to view
|
||||||
|
- Sort PGP keys and S/MIME certificates on email address
|
||||||
|
- Optionally use existing private key to generate S/MIME certificate
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Better handling to detect which PGP or S/MIME sign/encrypt to use
|
||||||
|
- Improved StorageType handling
|
||||||
|
- Cleanup and improved Capa handling
|
||||||
|
- OPEN_PGP should be OPENPGP as it is one word
|
||||||
|
- Use get_debug_type() instead of gettype()
|
||||||
|
- Require OpenSSL due to S/MIME
|
||||||
|
- AbstractProvider::IsActive() is now an abstract method and must be defined in child class
|
||||||
|
- Make better use of SnappyMail\SensitiveString
|
||||||
|
- Update Polish translation by @tinola
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Verify S/MIME signatures got broken allong the way while implementing this
|
||||||
|
- Generate S/MIME self-signed certificate failed to keep existing private key
|
||||||
|
- MIME parser RegExp didn't escape boundary which caused issues
|
||||||
|
- TypeError: b64Encode(...).match(...) is null on saving compose draft
|
||||||
|
- Fix timestampToString() for future dates
|
||||||
|
|
||||||
|
|
||||||
|
## 2.35.0 – 2024-02-20
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- S/MIME support
|
||||||
|
[#259](https://github.com/the-djmaze/snappymail/issues/259)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Disable IMAP METADATA by default (hardly used)
|
||||||
|
- Update Polish translation by @tinola
|
||||||
|
- Rename CSS .openpgp-control to .crypto-control
|
||||||
|
- Renamed some methods in PHP
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- When moving a folder/mailbox check for parent delimiter
|
||||||
|
- Mask `passphrase` in the logs for PHP < 8.2
|
||||||
|
- Added some missing translations
|
||||||
|
- Sign messages using PGP
|
||||||
|
- Check for CONDSTORE or QRESYNC to get the HIGHESTMODSEQ
|
||||||
|
- Unable to login on certain IMAP server since 2.34.2
|
||||||
|
[#1438](https://github.com/the-djmaze/snappymail/issues/1438)
|
||||||
|
|
||||||
|
### Nextcloud
|
||||||
|
- Save as .eml
|
||||||
|
[#1425](https://github.com/the-djmaze/snappymail/issues/1425)
|
||||||
|
|
||||||
|
|
||||||
|
## 2.34.2 – 2024-02-14
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Message was sent but not saved to sent items folder
|
||||||
|
[#1432](https://github.com/the-djmaze/snappymail/issues/1432)
|
||||||
|
- Login with scram failed
|
||||||
|
[#1433](https://github.com/the-djmaze/snappymail/issues/1433)
|
||||||
|
|
||||||
|
|
||||||
|
## 2.34.1 – 2024-02-13
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Autocrypt support
|
||||||
|
[#342](https://github.com/the-djmaze/snappymail/issues/342)
|
||||||
|
- Load the mailboxes/folders of all namespaces (other users, global, shared, etc.)
|
||||||
|
- Load keys from server into OpenPGP.js
|
||||||
|
[#973](https://github.com/the-djmaze/snappymail/issues/973)
|
||||||
|
- Import PGP Keys from remote key servers
|
||||||
|
- Sort Inbox Folders with Unread Messages First
|
||||||
|
[#1427](https://github.com/the-djmaze/snappymail/issues/1427)
|
||||||
|
- Define JMAP FolderModel.myRights
|
||||||
|
- Identity Management: add identity display name
|
||||||
|
[#1405](https://github.com/the-djmaze/snappymail/issues/1405)
|
||||||
|
- Identity Management: add per-identity "sent" folder
|
||||||
|
[#1404](https://github.com/the-djmaze/snappymail/issues/1404)
|
||||||
|
- Some support for JSON-LD / Structured Email
|
||||||
|
[#1422](https://github.com/the-djmaze/snappymail/issues/1422)
|
||||||
|
- Domain Autoconfig and Microsoft's autodiscover (and also as extension/plugin)
|
||||||
|
- View MMS messages that are received via email
|
||||||
|
[#1294](https://github.com/the-djmaze/snappymail/issues/1294)
|
||||||
|
- Draft code for S/MIME
|
||||||
|
[#259](https://github.com/the-djmaze/snappymail/issues/259)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Many OpenPGP improvements
|
||||||
|
[#89](https://github.com/the-djmaze/snappymail/issues/89)
|
||||||
|
- Allow CSP connect-src CORS for keys.openpgp.org to directly fetch PGP keys
|
||||||
|
- Improved handling of visible folders
|
||||||
|
- KnockoutJS Replace some ko.exportSymbol('*') in favour of ko['*']
|
||||||
|
- KnockoutJS use Symbol for isObservableArray()
|
||||||
|
- Simplify generating folderListVisible
|
||||||
|
- Drop the bSearchSecretWords param from logger
|
||||||
|
- Transparent background for text
|
||||||
|
[#1412](https://github.com/the-djmaze/snappymail/issues/1412)
|
||||||
|
- Enable OpenPGP.js by default at install
|
||||||
|
- Added folder edit popup for improved IMAP ACL Support
|
||||||
|
[#157](https://github.com/the-djmaze/snappymail/issues/157)
|
||||||
|
- Process all IMAP namespaces
|
||||||
|
- Update Polish by @tinola
|
||||||
|
- Update Portuguese by @ner00
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Make time_zone a select list due to PEBKAC
|
||||||
|
- Workaround Outlook generated double spacing
|
||||||
|
[#1415](https://github.com/the-djmaze/snappymail/issues/1415)
|
||||||
|
- HTML Parser is not picking up the full Unsubscribe URL in the attached text file
|
||||||
|
[#1225](https://github.com/the-djmaze/snappymail/issues/1225)
|
||||||
|
- Contacts - it auto "Select All", after entry delete
|
||||||
|
[#1411](https://github.com/the-djmaze/snappymail/issues/1411)
|
||||||
|
- Message header parsing issue
|
||||||
|
[#1403](https://github.com/the-djmaze/snappymail/issues/1403)
|
||||||
|
- apple-touch-icon should not be transparent
|
||||||
|
[#1408](https://github.com/the-djmaze/snappymail/issues/1408)
|
||||||
|
- Creation of dynamic property is deprecated
|
||||||
|
[#1409](https://github.com/the-djmaze/snappymail/issues/1409)
|
||||||
|
- Ask/send readReceipt was broken
|
||||||
|
- OpenPGP public key can not be removed anymore after importing private key of subkey
|
||||||
|
[#1384](https://github.com/the-djmaze/snappymail/issues/1384)
|
||||||
|
- KnockoutJS failed to output text '0'
|
||||||
|
- JavaScript friendlySize() failed on 0
|
||||||
|
- Workaround Dovecot `PREAUTH [CAPABILITY (null)]` issue
|
||||||
|
- Workaround disabled ACL could cause "Disconnected: Too many invalid IMAP commands"
|
||||||
|
|
||||||
|
### Nextcloud
|
||||||
|
- Save multiple as .eml
|
||||||
|
[#1425](https://github.com/the-djmaze/snappymail/issues/1425)
|
||||||
|
- Disabled support for Nextcloud OpenID Connect
|
||||||
|
[#1420](https://github.com/the-djmaze/snappymail/issues/1420)
|
||||||
|
|
||||||
|
|
||||||
|
## 2.33.0 – 2024-01-22
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Feature to use the SQLite AddressBook per login account instead of global (on by default).
|
||||||
|
- Return all fetched messages headers in JSON.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Docker hub use Alpine linux 3.18.5 and PHP 8.2
|
||||||
|
- Some InvalidArgumentException to the better suited ValueError
|
||||||
|
- Removed some unused KnockoutJS code
|
||||||
|
- KnockoutJS drop unused rateLimit method
|
||||||
|
- Cleanup some data-bind=""
|
||||||
|
- Drop the disabled KnockoutJS twoWayBindings
|
||||||
|
- Drop support for KnockoutJS _ko_property_writers and for two-way binding they must be observables
|
||||||
|
- Login form use method="POST" to prevent uri exposure when javascript fails
|
||||||
|
- Merge code to generate MIME PGP parts and MIME Plain parts
|
||||||
|
- SMTP sendRequestWithCheck for future support of RFC's
|
||||||
|
- Cleanup mime header handling
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Sorting not supported since 2.32.0
|
||||||
|
[#1373](https://github.com/the-djmaze/snappymail/issues/1373)
|
||||||
|
- FILE_ON_SAVING_ERROR is not defined
|
||||||
|
[#1379](https://github.com/the-djmaze/snappymail/issues/1379)
|
||||||
|
- Saving EML files with same subject result in only saving latest email
|
||||||
|
[#1381](https://github.com/the-djmaze/snappymail/issues/1381)
|
||||||
|
- Some Sieve parser issues
|
||||||
|
- Handling of RainLoop Sieve script
|
||||||
|
- Sieve rfc5429 RejectCommand and ErejectCommand
|
||||||
|
- KnockoutJS title:value was removed, use attr:{title:value}
|
||||||
|
- dataBaseUpgrade() always runs on sqlite and pgsql
|
||||||
|
- Message was sent but not saved to sent items folder
|
||||||
|
[#1397](https://github.com/the-djmaze/snappymail/issues/1397)
|
||||||
|
- DKIM `pass` detection sometimes failed
|
||||||
|
|
||||||
|
|
||||||
## 2.32.0 – 2023-12-26
|
## 2.32.0 – 2023-12-26
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
|
||||||
44
README.md
44
README.md
|
|
@ -49,7 +49,7 @@ This fork of RainLoop has the following changes:
|
||||||
* Admin uses password_hash/password_verify
|
* Admin uses password_hash/password_verify
|
||||||
* Auth failed attempts written to syslog
|
* Auth failed attempts written to syslog
|
||||||
* Added Fail2ban instructions
|
* Added Fail2ban instructions
|
||||||
* ES2018
|
* ES2020
|
||||||
* PHP 7.4+ required
|
* PHP 7.4+ required
|
||||||
* PHP mbstring extension required
|
* PHP mbstring extension required
|
||||||
* PHP replaced pclZip with PharData and ZipArchive
|
* PHP replaced pclZip with PharData and ZipArchive
|
||||||
|
|
@ -140,28 +140,28 @@ RainLoop 1.17 vs SnappyMail
|
||||||
|
|
||||||
|js/* |RainLoop |Snappy |
|
|js/* |RainLoop |Snappy |
|
||||||
|--------------- |--------: |--------: |
|
|--------------- |--------: |--------: |
|
||||||
|admin.js |2.170.153 | 80.370 |
|
|admin.js |2.170.153 | 83.494 |
|
||||||
|app.js |4.207.787 | 408.429 |
|
|app.js |4.207.787 | 437.079 |
|
||||||
|boot.js | 868.735 | 4.142 |
|
|boot.js | 868.735 | 4.147 |
|
||||||
|libs.js | 658.812 | 193.230 |
|
|libs.js | 658.812 | 193.716 |
|
||||||
|sieve.js | 0 | 85.085 |
|
|sieve.js | 0 | 84.598 |
|
||||||
|polyfills.js | 334.608 | 0 |
|
|polyfills.js | 334.608 | 0 |
|
||||||
|serviceworker.js | 0 | 285 |
|
|serviceworker.js | 0 | 285 |
|
||||||
|TOTAL |8.240.095 | 771.541 |
|
|TOTAL |8.240.095 | 803.319 |
|
||||||
|
|
||||||
|js/min/* |RainLoop |Snappy |RL gzip |SM gzip |RL brotli |SM brotli |
|
|js/min/* |RainLoop |Snappy |RL gzip |SM gzip |RL brotli |SM brotli |
|
||||||
|--------------- |--------: |--------: |------: |------: |--------: |--------: |
|
|--------------- |--------: |--------: |------: |------: |--------: |--------: |
|
||||||
|admin.min.js | 256.831 | 39.285 | 73.606 | 13.183 | 60.877 | 11.806 |
|
|admin.min.js | 256.831 | 40.856 | 73.606 | 13.776 | 60.877 | 12.345 |
|
||||||
|app.min.js | 515.367 | 186.270 |139.456 | 63.111 |110.485 | 54.218 |
|
|app.min.js | 515.367 | 197.782 |139.456 | 67.143 |110.485 | 57.251 |
|
||||||
|boot.min.js | 84.659 | 2.084 | 26.998 | 1.202 | 23.643 | 1.003 |
|
|boot.min.js | 84.659 | 2.087 | 26.998 | 1.204 | 23.643 | 1.002 |
|
||||||
|libs.min.js | 584.772 | 93.401 |180.901 | 34.765 |155.182 | 31.194 |
|
|libs.min.js | 584.772 | 92.365 |180.901 | 34.487 |155.182 | 30.830 |
|
||||||
|sieve.min.js | 0 | 41.316 | 0 | 10.364 | 0 | 9.352 |
|
|sieve.min.js | 0 | 41.093 | 0 | 10.325 | 0 | 9.327 |
|
||||||
|polyfills.min.js | 32.837 | 0 | 11.406 | 0 | 10.175 | 0 |
|
|polyfills.min.js | 32.837 | 0 | 11.406 | 0 | 10.175 | 0 |
|
||||||
|TOTAL user |1.217.635 | 281.755 |358.761 | 99.078 |299.485 | 86.415 |
|
|TOTAL user |1.217.635 | 292.234 |358.761 |102.834 |299.485 | 89.083 |
|
||||||
|TOTAL user+sieve |1.217.635 | 323.071 |358.761 |109.442 |299.485 | 95.767 |
|
|TOTAL user+sieve |1.217.635 | 333.327 |358.761 |113.159 |299.485 | 98.410 |
|
||||||
|TOTAL admin | 959.099 | 134.770 |292.911 | 49.150 |249.877 | 44.003 |
|
|TOTAL admin | 959.099 | 135.308 |292.911 | 49.467 |249.877 | 44.177 |
|
||||||
|
|
||||||
For a user it is around 69% smaller and faster than traditional RainLoop.
|
For a user it is around 68% smaller and faster than traditional RainLoop.
|
||||||
|
|
||||||
### CSS changes
|
### CSS changes
|
||||||
|
|
||||||
|
|
@ -188,12 +188,12 @@ For a user it is around 69% smaller and faster than traditional RainLoop.
|
||||||
|
|
||||||
|css/* |RainLoop |Snappy |RL gzip |SM gzip |SM brotli |
|
|css/* |RainLoop |Snappy |RL gzip |SM gzip |SM brotli |
|
||||||
|------------ |-------: |------: |------: |------: |--------: |
|
|------------ |-------: |------: |------: |------: |--------: |
|
||||||
|app.css | 340.331 | 84.484 | 46.946 | 17.627 | 15.110 |
|
|app.css | 340.331 | 84.629 | 46.946 | 17.652 | 15.127 |
|
||||||
|app.min.css | 274.947 | 67.910 | 39.647 | 15.541 | 13.547 |
|
|app.min.css | 274.947 | 67.995 | 39.647 | 15.562 | 13.568 |
|
||||||
|boot.css | | 1.326 | | 664 | 545 |
|
|boot.css | | 1.326 | | 664 | 545 |
|
||||||
|boot.min.css | | 1.071 | | 590 | 474 |
|
|boot.min.css | | 1.071 | | 590 | 474 |
|
||||||
|admin.css | | 30.641 | | 7.028 | 6.111 |
|
|admin.css | | 30.528 | | 7.001 | 6.088 |
|
||||||
|admin.min.css | | 24.742 | | 6.352 | 5.599 |
|
|admin.min.css | | 24.652 | | 6.325 | 5.574 |
|
||||||
|
|
||||||
### PGP
|
### PGP
|
||||||
RainLoop uses the old OpenPGP.js v2
|
RainLoop uses the old OpenPGP.js v2
|
||||||
|
|
@ -207,7 +207,7 @@ See https://github.com/the-djmaze/openpgpjs for development
|
||||||
|
|
||||||
|OpenPGP |RainLoop |Snappy |RL gzip |SM gzip |RL brotli |SM brotli |
|
|OpenPGP |RainLoop |Snappy |RL gzip |SM gzip |RL brotli |SM brotli |
|
||||||
|--------------- |--------: |--------: |------: |-------: |--------: |--------: |
|
|--------------- |--------: |--------: |------: |-------: |--------: |--------: |
|
||||||
|openpgp.min.js | 330.742 | 541.176 |102.388 | 168.266 | 84.241 | 138.278 |
|
|openpgp.min.js | 330.742 | 546.309 |102.388 | 169.249 | 84.241 | 138.751 |
|
||||||
|openpgp.worker | 1.499 | | 824 | | 695 | |
|
|openpgp.worker | 1.499 | | 824 | | 695 | |
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -224,5 +224,3 @@ Still TODO:
|
||||||
|ckeditor | ? | 520.035 | ? | 155.916 |
|
|ckeditor | ? | 520.035 | ? | 155.916 |
|
||||||
|
|
||||||
CKEditor including the 7 asset requests (css,language,plugins,icons) is 633.46 KB / 180.47 KB (gzip).
|
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.
|
|
||||||
|
|
|
||||||
|
|
@ -6,15 +6,15 @@ Currently due to the fast development only the latest version receives security
|
||||||
|
|
||||||
| Version | Supported |
|
| Version | Supported |
|
||||||
| -------- | --------- |
|
| -------- | --------- |
|
||||||
| 2.13.x | ✔ |
|
| 2.30.x | ✔ |
|
||||||
| < 2.13.0 | ❌ |
|
| < 2.30.0 | ❌ |
|
||||||
|
|
||||||
## Reporting a Vulnerability
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
Please report security issues or vulnerabilities as an encrypted email to [security@snappymail.eu](mailto:security@snappymail.eu).
|
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.
|
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-----
|
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||||
Comment: Type: 255-bit EdDSA
|
Comment: Type: 255-bit EdDSA
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ import ko from 'ko';
|
||||||
import { logoutLink } from 'Common/Links';
|
import { logoutLink } from 'Common/Links';
|
||||||
import { i18nToNodes, initOnStartOrLangChange } from 'Common/Translator';
|
import { i18nToNodes, initOnStartOrLangChange } from 'Common/Translator';
|
||||||
|
|
||||||
|
import { arePopupsVisible } from 'Knoin/Knoin';
|
||||||
|
|
||||||
import { LanguageStore } from 'Stores/Language';
|
import { LanguageStore } from 'Stores/Language';
|
||||||
import { initThemes } from 'Stores/Theme';
|
import { initThemes } from 'Stores/Theme';
|
||||||
|
|
||||||
|
|
@ -18,6 +20,7 @@ export class AbstractApp {
|
||||||
}
|
}
|
||||||
|
|
||||||
logoutReload(url) {
|
logoutReload(url) {
|
||||||
|
arePopupsVisible(false);
|
||||||
url = url || logoutLink();
|
url = url || logoutLink();
|
||||||
if (location.href !== url) {
|
if (location.href !== url) {
|
||||||
setTimeout(() => location.href = url, 100);
|
setTimeout(() => location.href = url, 100);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import 'External/ko';
|
import 'External/ko';
|
||||||
|
|
||||||
import { Settings, SettingsGet } from 'Common/Globals';
|
import { SettingsGet, SettingsAdmin } from 'Common/Globals';
|
||||||
import { initThemes } from 'Stores/Theme';
|
import { initThemes } from 'Stores/Theme';
|
||||||
|
|
||||||
import Remote from 'Remote/Admin/Fetch';
|
import Remote from 'Remote/Admin/Fetch';
|
||||||
|
|
@ -11,6 +11,8 @@ import { LoginAdminScreen } from 'Screen/Admin/Login';
|
||||||
import { startScreens } from 'Knoin/Knoin';
|
import { startScreens } from 'Knoin/Knoin';
|
||||||
import { AbstractApp } from 'App/Abstract';
|
import { AbstractApp } from 'App/Abstract';
|
||||||
|
|
||||||
|
import { AskPopupView } from 'View/Popup/Ask';
|
||||||
|
|
||||||
export class AdminApp extends AbstractApp {
|
export class AdminApp extends AbstractApp {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(Remote);
|
super(Remote);
|
||||||
|
|
@ -23,7 +25,8 @@ export class AdminApp extends AbstractApp {
|
||||||
}
|
}
|
||||||
|
|
||||||
start() {
|
start() {
|
||||||
if (!Settings.app('adminAllowed')) {
|
// if (!Settings.app('adminAllowed')) {
|
||||||
|
if (!SettingsAdmin('allowed')) {
|
||||||
rl.route.root();
|
rl.route.root();
|
||||||
setTimeout(() => location.href = '/', 1);
|
setTimeout(() => location.href = '/', 1);
|
||||||
} else if (SettingsGet('Auth')) {
|
} else if (SettingsGet('Auth')) {
|
||||||
|
|
@ -34,3 +37,16 @@ export class AdminApp extends AbstractApp {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import 'External/User/ko';
|
import 'External/User/ko';
|
||||||
|
|
||||||
import { SMAudio } from 'Common/Audio';
|
import { SMAudio } from 'Common/Audio';
|
||||||
import { isArray, pInt } from 'Common/Utils';
|
import { pInt } from 'Common/Utils';
|
||||||
import { mailToHelper, setLayoutResizer, dropdownsDetectVisibility } from 'Common/UtilsUser';
|
import { mailToHelper, setLayoutResizer, dropdownsDetectVisibility, loadAccountsAndIdentities } from 'Common/UtilsUser';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
FolderType,
|
FolderType,
|
||||||
|
|
@ -33,9 +33,9 @@ import { SettingsUserStore } from 'Stores/User/Settings';
|
||||||
import { NotificationUserStore } from 'Stores/User/Notification';
|
import { NotificationUserStore } from 'Stores/User/Notification';
|
||||||
import { AccountUserStore } from 'Stores/User/Account';
|
import { AccountUserStore } from 'Stores/User/Account';
|
||||||
import { ContactUserStore } from 'Stores/User/Contact';
|
import { ContactUserStore } from 'Stores/User/Contact';
|
||||||
import { IdentityUserStore } from 'Stores/User/Identity';
|
|
||||||
import { FolderUserStore } from 'Stores/User/Folder';
|
import { FolderUserStore } from 'Stores/User/Folder';
|
||||||
import { PgpUserStore } from 'Stores/User/Pgp';
|
import { PgpUserStore } from 'Stores/User/Pgp';
|
||||||
|
import { SMimeUserStore } from 'Stores/User/SMime';
|
||||||
import { MessagelistUserStore } from 'Stores/User/Messagelist';
|
import { MessagelistUserStore } from 'Stores/User/Messagelist';
|
||||||
import { ThemeStore, initThemes } from 'Stores/Theme';
|
import { ThemeStore, initThemes } from 'Stores/Theme';
|
||||||
import { LanguageStore } from 'Stores/Language';
|
import { LanguageStore } from 'Stores/Language';
|
||||||
|
|
@ -43,9 +43,6 @@ import { MessageUserStore } from 'Stores/User/Message';
|
||||||
|
|
||||||
import Remote from 'Remote/User/Fetch';
|
import Remote from 'Remote/User/Fetch';
|
||||||
|
|
||||||
import { AccountModel } from 'Model/Account';
|
|
||||||
import { IdentityModel } from 'Model/Identity';
|
|
||||||
|
|
||||||
import { LoginUserScreen } from 'Screen/User/Login';
|
import { LoginUserScreen } from 'Screen/User/Login';
|
||||||
import { MailBoxUserScreen } from 'Screen/User/MailBox';
|
import { MailBoxUserScreen } from 'Screen/User/MailBox';
|
||||||
import { SettingsUserScreen } from 'Screen/User/Settings';
|
import { SettingsUserScreen } from 'Screen/User/Settings';
|
||||||
|
|
@ -89,6 +86,8 @@ export class AppUser extends AbstractApp {
|
||||||
|
|
||||||
this.folderList = FolderUserStore.folderList;
|
this.folderList = FolderUserStore.folderList;
|
||||||
this.messageList = MessagelistUserStore;
|
this.messageList = MessagelistUserStore;
|
||||||
|
|
||||||
|
this.ask = AskPopupView;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -139,31 +138,6 @@ export class AppUser extends AbstractApp {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 => IdentityModel.reviveFromJson(identityData))
|
|
||||||
: []
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} folder
|
* @param {string} folder
|
||||||
* @param {Array=} list = []
|
* @param {Array=} list = []
|
||||||
|
|
@ -216,9 +190,7 @@ export class AppUser extends AbstractApp {
|
||||||
|
|
||||||
setRefreshFoldersInterval(pInt(SettingsGet('CheckMailInterval')));
|
setRefreshFoldersInterval(pInt(SettingsGet('CheckMailInterval')));
|
||||||
|
|
||||||
ContactUserStore.init();
|
loadAccountsAndIdentities();
|
||||||
|
|
||||||
this.accountsAndIdentities();
|
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const cF = FolderUserStore.currentFolderFullName();
|
const cF = FolderUserStore.currentFolderFullName();
|
||||||
|
|
@ -247,6 +219,7 @@ export class AppUser extends AbstractApp {
|
||||||
setInterval(reloadTime, 60000);
|
setInterval(reloadTime, 60000);
|
||||||
|
|
||||||
PgpUserStore.init();
|
PgpUserStore.init();
|
||||||
|
SMimeUserStore.loadCertificates();
|
||||||
|
|
||||||
setTimeout(() => mailToHelper(SettingsGet('mailToEmail')), 500);
|
setTimeout(() => mailToHelper(SettingsGet('mailToEmail')), 500);
|
||||||
|
|
||||||
|
|
@ -267,7 +240,6 @@ export class AppUser extends AbstractApp {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
startScreens([LoginUserScreen]);
|
startScreens([LoginUserScreen]);
|
||||||
}
|
}
|
||||||
|
|
@ -278,3 +250,50 @@ export class AppUser extends AbstractApp {
|
||||||
showScreenPopup(ComposePopupView, params);
|
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()
|
||||||
|
}),
|
||||||
|
() => 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();
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,7 @@ Notifications = {
|
||||||
ConnectionError: 104,
|
ConnectionError: 104,
|
||||||
DomainNotAllowed: 109,
|
DomainNotAllowed: 109,
|
||||||
AccountNotAllowed: 110,
|
AccountNotAllowed: 110,
|
||||||
|
CryptKeyError: 111,
|
||||||
|
|
||||||
ContactsSyncError: 140,
|
ContactsSyncError: 140,
|
||||||
|
|
||||||
|
|
@ -95,7 +96,6 @@ Notifications = {
|
||||||
JsonParse: 952,
|
JsonParse: 952,
|
||||||
// JsonTimeout: 953,
|
// JsonTimeout: 953,
|
||||||
|
|
||||||
UnknownNotification: 998,
|
|
||||||
UnknownError: 999,
|
UnknownError: 999,
|
||||||
|
|
||||||
// Admin
|
// Admin
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
/* eslint key-spacing: 0 */
|
/* eslint key-spacing: 0 */
|
||||||
/* eslint quote-props: 0 */
|
/* eslint quote-props: 0 */
|
||||||
|
|
||||||
import { arrayLength } from 'Common/Utils';
|
import { arrayLength, pInt } from 'Common/Utils';
|
||||||
|
|
||||||
export const RFC822 = 'message/rfc822';
|
export const RFC822 = 'message/rfc822';
|
||||||
|
|
||||||
|
|
@ -30,6 +30,8 @@ const
|
||||||
p7c: app+'pkcs7-mime',
|
p7c: app+'pkcs7-mime',
|
||||||
p7m: app+'pkcs7-mime',
|
p7m: app+'pkcs7-mime',
|
||||||
p7s: app+'pkcs7-signature',
|
p7s: app+'pkcs7-signature',
|
||||||
|
p12: app+'pkcs12',
|
||||||
|
pfx: app+'x-pkcs12',
|
||||||
torrent: app+'x-bittorrent',
|
torrent: app+'x-bittorrent',
|
||||||
|
|
||||||
// scripts
|
// scripts
|
||||||
|
|
@ -136,7 +138,7 @@ export const FileInfo = {
|
||||||
getContentType: fileName => {
|
getContentType: fileName => {
|
||||||
fileName = lowerCase(fileName);
|
fileName = lowerCase(fileName);
|
||||||
if ('winmail.dat' === fileName) {
|
if ('winmail.dat' === fileName) {
|
||||||
return app + 'ms-tnef';
|
return app + 'vnd.ms-tnef';
|
||||||
}
|
}
|
||||||
let ext = fileName.split('.').pop();
|
let ext = fileName.split('.').pop();
|
||||||
if (/^(txt|text|def|list|in|ini|log|sql|cfg|conf)$/.test(ext))
|
if (/^(txt|text|def|list|in|ini|log|sql|cfg|conf)$/.test(ext))
|
||||||
|
|
@ -164,7 +166,7 @@ export const FileInfo = {
|
||||||
*/
|
*/
|
||||||
getType: (ext, mimeType) => {
|
getType: (ext, mimeType) => {
|
||||||
ext = lowerCase(ext);
|
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;
|
let key = ext + mimeType;
|
||||||
if (cache[key]) {
|
if (cache[key]) {
|
||||||
|
|
@ -173,7 +175,7 @@ export const FileInfo = {
|
||||||
|
|
||||||
let result = FileType.Unknown;
|
let result = FileType.Unknown;
|
||||||
const mimeTypeParts = mimeType.split('/'),
|
const mimeTypeParts = mimeType.split('/'),
|
||||||
type = mimeTypeParts[1].replace('x-','').replace('-compressed',''),
|
type = mimeTypeParts[1].replace('-compressed',''),
|
||||||
match = str => mimeType.includes(str),
|
match = str => mimeType.includes(str),
|
||||||
archive = /^(zip|7z|tar|rar|gzip|bzip|bzip2)$/;
|
archive = /^(zip|7z|tar|rar|gzip|bzip|bzip2)$/;
|
||||||
|
|
||||||
|
|
@ -205,9 +207,8 @@ export const FileInfo = {
|
||||||
case 'pdf' == type || 'pdf' == ext:
|
case 'pdf' == type || 'pdf' == ext:
|
||||||
result = FileType.Pdf;
|
result = FileType.Pdf;
|
||||||
break;
|
break;
|
||||||
case [app+'pgp-signature', app+'pgp-keys'].includes(mimeType)
|
case [app+'pgp-signature', app+'pgp-keys', exts.p7m, exts.p7s, exts.p12, exts.pfx].includes(mimeType)
|
||||||
|| ['asc', 'pem', 'ppk'].includes(ext)
|
|| ['asc', 'pem', 'ppk', 'p7s', 'p7m', 'p12', 'pfx'].includes(ext):
|
||||||
|| [app+'pkcs7-signature'].includes(mimeType) || 'p7s' == ext:
|
|
||||||
result = FileType.Certificate;
|
result = FileType.Certificate;
|
||||||
break;
|
break;
|
||||||
case match(msOffice+'.wordprocessingml') || match(openDoc+'.text') || match('vnd.ms-word')
|
case match(msOffice+'.wordprocessingml') || match(openDoc+'.text') || match('vnd.ms-word')
|
||||||
|
|
@ -273,8 +274,8 @@ export const FileInfo = {
|
||||||
},
|
},
|
||||||
|
|
||||||
friendlySize: bytes => {
|
friendlySize: bytes => {
|
||||||
bytes = parseInt(bytes, 10) || 0;
|
bytes = pInt(bytes);
|
||||||
let i = Math.floor(Math.log(bytes) / Math.log(1024));
|
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];
|
return (bytes / Math.pow(1024, i)).toFixed(2>i ? 0 : 1) + ' ' + sizes[i];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { RFC822 } from 'Common/File';
|
import { RFC822 } from 'Common/File';
|
||||||
import { getFolderInboxName, getFolderFromCacheList } from 'Common/Cache';
|
import { getFolderInboxName, getFolderFromCacheList } from 'Common/Cache';
|
||||||
|
import { baseCollator } from 'Common/Translator';
|
||||||
import { isArray, arrayLength } from 'Common/Utils';
|
import { isArray, arrayLength } from 'Common/Utils';
|
||||||
import { SettingsUserStore } from 'Stores/User/Settings';
|
import { SettingsUserStore } from 'Stores/User/Settings';
|
||||||
import { FolderUserStore } from 'Stores/User/Folder';
|
import { FolderUserStore } from 'Stores/User/Folder';
|
||||||
|
|
@ -27,7 +28,7 @@ setRefreshFoldersInterval = minutes => {
|
||||||
|
|
||||||
sortFolders = folders => {
|
sortFolders = folders => {
|
||||||
try {
|
try {
|
||||||
let collator = new Intl.Collator(undefined, {numeric: true, sensitivity: 'base'});
|
let collator = baseCollator(true);
|
||||||
folders.sort((a, b) =>
|
folders.sort((a, b) =>
|
||||||
a.isInbox() ? -1 : (b.isInbox() ? 1 : collator.compare(a.fullName, b.fullName))
|
a.isInbox() ? -1 : (b.isInbox() ? 1 : collator.compare(a.fullName, b.fullName))
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ export const
|
||||||
|
|
||||||
Settings = rl.settings,
|
Settings = rl.settings,
|
||||||
SettingsGet = Settings.get,
|
SettingsGet = Settings.get,
|
||||||
|
SettingsAdmin = name => (SettingsGet('Admin') || {})[name],
|
||||||
SettingsCapa = name => name && !!(SettingsGet('Capa') || {})[name],
|
SettingsCapa = name => name && !!(SettingsGet('Capa') || {})[name],
|
||||||
|
|
||||||
dropdowns = [],
|
dropdowns = [],
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { createElement } from 'Common/Globals';
|
import { createElement } from 'Common/Globals';
|
||||||
import { forEachObjectEntry, pInt } from 'Common/Utils';
|
import { forEachObjectEntry, isArray, pInt } from 'Common/Utils';
|
||||||
import { SettingsUserStore } from 'Stores/User/Settings';
|
import { SettingsUserStore } from 'Stores/User/Settings';
|
||||||
|
|
||||||
const
|
const
|
||||||
|
|
@ -207,7 +207,9 @@ export const
|
||||||
bqLevel = parseInt(SettingsUserStore.maxBlockquotesLevel()),
|
bqLevel = parseInt(SettingsUserStore.maxBlockquotesLevel()),
|
||||||
|
|
||||||
result = {
|
result = {
|
||||||
hasExternals: false
|
hasExternals: false,
|
||||||
|
tracking: false,
|
||||||
|
linkedData: []
|
||||||
},
|
},
|
||||||
|
|
||||||
findAttachmentByCid = cId => oAttachments.findByCid(cId),
|
findAttachmentByCid = cId => oAttachments.findByCid(cId),
|
||||||
|
|
@ -269,12 +271,15 @@ export const
|
||||||
// Not supported by <template> element
|
// Not supported by <template> element
|
||||||
// .replace(/<!doctype[^>]*>/gi, '')
|
// .replace(/<!doctype[^>]*>/gi, '')
|
||||||
// .replace(/<\?xml[^>]*\?>/gi, '')
|
// .replace(/<\?xml[^>]*\?>/gi, '')
|
||||||
|
.replace(/<(\/?)head(\s[^>]*)?>/gi, '')
|
||||||
.replace(/<(\/?)body(\s[^>]*)?>/gi, '<$1div class="mail-body"$2>')
|
.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
|
// Fix Reddit https://github.com/the-djmaze/snappymail/issues/540
|
||||||
.replace(/<span class="preview-text"[\s\S]+?<\/span>/, '')
|
.replace(/<span class="preview-text"[\s\S]+?<\/span>/, '')
|
||||||
// https://github.com/the-djmaze/snappymail/issues/900
|
// https://github.com/the-djmaze/snappymail/issues/900
|
||||||
.replace(/\u2028/g,' ')
|
.replace(/\u2028/g,' ')
|
||||||
|
// https://github.com/the-djmaze/snappymail/issues/1415
|
||||||
|
.replace(/<br>\s*<\/p>/gi,'</p>')
|
||||||
.trim();
|
.trim();
|
||||||
html = '';
|
html = '';
|
||||||
|
|
||||||
|
|
@ -284,6 +289,21 @@ export const
|
||||||
nodeIterator.referenceNode.remove();
|
nodeIterator.referenceNode.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
tmpl.content.querySelectorAll(
|
tmpl.content.querySelectorAll(
|
||||||
disallowedTags
|
disallowedTags
|
||||||
+ (0 < bqLevel ? ',' + (new Array(1 + bqLevel).fill('blockquote').join(' ')) : '')
|
+ (0 < bqLevel ? ',' + (new Array(1 + bqLevel).fill('blockquote').join(' ')) : '')
|
||||||
|
|
@ -381,10 +401,14 @@ export const
|
||||||
if ('A' === name) {
|
if ('A' === name) {
|
||||||
value = oElement.href;
|
value = oElement.href;
|
||||||
if (!/^([a-z]+):/i.test(value)) {
|
if (!/^([a-z]+):/i.test(value)) {
|
||||||
setAttribute('data-x-broken-href', value);
|
setAttribute('data-x-href-broken', value);
|
||||||
delAttribute('href');
|
delAttribute('href');
|
||||||
} else {
|
} else {
|
||||||
oElement.href = stripTracking(value);
|
oElement.href = stripTracking(value);
|
||||||
|
if (oElement.href != value) {
|
||||||
|
result.tracking = true;
|
||||||
|
setAttribute('data-x-href-tracking', value);
|
||||||
|
}
|
||||||
setAttribute('target', '_blank');
|
setAttribute('target', '_blank');
|
||||||
// setAttribute('rel', 'external nofollow noopener noreferrer');
|
// setAttribute('rel', 'external nofollow noopener noreferrer');
|
||||||
}
|
}
|
||||||
|
|
@ -406,9 +430,8 @@ export const
|
||||||
*/
|
*/
|
||||||
|
|
||||||
let skipStyle = false;
|
let skipStyle = false;
|
||||||
if (hasAttribute('src')) {
|
value = delAttribute('src');
|
||||||
value = stripTracking(delAttribute('src'));
|
if (value) {
|
||||||
|
|
||||||
if ('IMG' === name) {
|
if ('IMG' === name) {
|
||||||
oElement.loading = 'lazy';
|
oElement.loading = 'lazy';
|
||||||
let attachment;
|
let attachment;
|
||||||
|
|
@ -445,12 +468,18 @@ export const
|
||||||
oStyle.display = 'none';
|
oStyle.display = 'none';
|
||||||
// setAttribute('style', 'display:none');
|
// setAttribute('style', 'display:none');
|
||||||
setAttribute('data-x-src-hidden', value);
|
setAttribute('data-x-src-hidden', value);
|
||||||
|
// result.tracking = true;
|
||||||
}
|
}
|
||||||
else if (httpre.test(value))
|
else if (httpre.test(value))
|
||||||
{
|
{
|
||||||
setAttribute('data-x-src', 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;
|
result.hasExternals = true;
|
||||||
oElement.alt || (oElement.alt = value.replace(/^.+\/([^/?]+).*$/, '$1').slice(-20));
|
oElement.alt || (oElement.alt = src.replace(/^.+\/([^/?]+).*$/, '$1').slice(-20));
|
||||||
}
|
}
|
||||||
else if (value.startsWith('data:image/'))
|
else if (value.startsWith('data:image/'))
|
||||||
{
|
{
|
||||||
|
|
@ -721,184 +750,7 @@ export const
|
||||||
.replace(/\n/g, '<br>');
|
.replace(/\n/g, '<br>');
|
||||||
blockquoteSwitcher();
|
blockquoteSwitcher();
|
||||||
return tmpl.innerHTML.trim();
|
return tmpl.innerHTML.trim();
|
||||||
},
|
};
|
||||||
|
|
||||||
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(''));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
rl.Utils = {
|
rl.Utils = {
|
||||||
htmlToPlain: htmlToPlain,
|
htmlToPlain: htmlToPlain,
|
||||||
|
|
|
||||||
161
dev/Common/HtmlEditor.js
Normal file
161
dev/Common/HtmlEditor.js
Normal 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(''));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
import { pInt } from 'Common/Utils';
|
import { pInt } from 'Common/Utils';
|
||||||
import { doc, Settings } from 'Common/Globals';
|
import { doc, Settings, SettingsAdmin } from 'Common/Globals';
|
||||||
|
|
||||||
const
|
const
|
||||||
BASE = doc.location.pathname.replace(/\/+$/,'') + '/',
|
BASE = doc.location.pathname.replace(/\/+$/,'') + '/',
|
||||||
HASH_PREFIX = '#/',
|
HASH_PREFIX = '#/',
|
||||||
|
|
||||||
adminPath = () => rl.adminArea() && !Settings.app('adminHost'),
|
adminPath = () => rl.adminArea() && !SettingsAdmin('host'),
|
||||||
|
|
||||||
prefix = () => BASE + '?' + (adminPath() ? Settings.app('adminPath') : '');
|
prefix = () => BASE + '?' + (adminPath() ? SettingsAdmin('path') : '');
|
||||||
|
|
||||||
export const
|
export const
|
||||||
SUB_QUERY_PREFIX = '&q[]=',
|
SUB_QUERY_PREFIX = '&q[]=',
|
||||||
|
|
@ -38,11 +38,10 @@ export const
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} download
|
* @param {string} download
|
||||||
* @param {string=} customSpecSuffix
|
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
attachmentDownload = (download, customSpecSuffix) =>
|
attachmentDownload = (download) =>
|
||||||
serverRequestRaw('Download', download, customSpecSuffix),
|
serverRequestRaw('Download', download),
|
||||||
|
|
||||||
proxy = url =>
|
proxy = url =>
|
||||||
BASE + '?/ProxyExternal/'
|
BASE + '?/ProxyExternal/'
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ const
|
||||||
|
|
||||||
getNotificationMessage = code => {
|
getNotificationMessage = code => {
|
||||||
let key = getKeyByValue(Notifications, code);
|
let key = getKeyByValue(Notifications, code);
|
||||||
return key ? I18N_DATA.NOTIFICATIONS[i18nKey(key).replace('_NOTIFICATION', '_ERROR')] : '';
|
return key ? I18N_DATA.NOTIFICATIONS[key] : '';
|
||||||
},
|
},
|
||||||
|
|
||||||
fromNow = date => relativeTime(Math.round((date.getTime() - Date.now()) / 1000));
|
fromNow = date => relativeTime(Math.round((date.getTime() - Date.now()) / 1000));
|
||||||
|
|
@ -45,8 +45,7 @@ export const
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (Intl.RelativeTimeFormat) {
|
if (Intl.RelativeTimeFormat) {
|
||||||
let rtf = new Intl.RelativeTimeFormat(doc.documentElement.lang);
|
return (new Intl.RelativeTimeFormat(doc.documentElement.lang)).format(seconds, unit);
|
||||||
return rtf.format(seconds, unit);
|
|
||||||
}
|
}
|
||||||
// Safari < 14
|
// Safari < 14
|
||||||
abs = Math.abs(seconds);
|
abs = Math.abs(seconds);
|
||||||
|
|
@ -62,7 +61,7 @@ export const
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
i18n = (key, valueList, defaulValue) => {
|
i18n = (key, valueList, defaulValue) => {
|
||||||
let result = null == defaulValue ? key : defaulValue;
|
let result = defaulValue ?? key;
|
||||||
let path = key.split('/');
|
let path = key.split('/');
|
||||||
if (I18N_DATA[path[0]] && path[1]) {
|
if (I18N_DATA[path[0]] && path[1]) {
|
||||||
result = I18N_DATA[path[0]][path[1]] || result;
|
result = I18N_DATA[path[0]][path[1]] || result;
|
||||||
|
|
@ -102,8 +101,7 @@ export const
|
||||||
|
|
||||||
timestampToString = (timeStampInUTC, formatStr) => {
|
timestampToString = (timeStampInUTC, formatStr) => {
|
||||||
const now = Date.now(),
|
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) {
|
if (31536000000 < time) {
|
||||||
const m = new Date(time), h = LanguageStore.hourCycle();
|
const m = new Date(time), h = LanguageStore.hourCycle();
|
||||||
switch (formatStr) {
|
switch (formatStr) {
|
||||||
|
|
@ -144,7 +142,7 @@ export const
|
||||||
time = Date.parse(element.dateTime) / 1000;
|
time = Date.parse(element.dateTime) / 1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
let key = element.dataset.momentFormat;
|
let key = element.dataset.timeFormat;
|
||||||
if (key) {
|
if (key) {
|
||||||
element.textContent = timestampToString(time, key);
|
element.textContent = timestampToString(time, key);
|
||||||
if ('FULL' !== key && 'FROMNOW' !== key) {
|
if ('FULL' !== key && 'FROMNOW' !== key) {
|
||||||
|
|
@ -212,14 +210,13 @@ export const
|
||||||
script.remove();
|
script.remove();
|
||||||
resolve();
|
resolve();
|
||||||
};
|
};
|
||||||
script.onerror = () => reject(new Error('Language '+language+' failed'));
|
script.onerror = () => reject(Error('Language '+language+' failed'));
|
||||||
script.src = langLink(language, admin);
|
script.src = langLink(language, admin);
|
||||||
// script.async = true;
|
// script.async = true;
|
||||||
doc.head.append(script);
|
doc.head.append(script);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
|
||||||
* @param {string} language
|
* @param {string} language
|
||||||
* @param {boolean=} isEng = false
|
* @param {boolean=} isEng = false
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
|
|
@ -229,6 +226,8 @@ export const
|
||||||
'LANGS_NAMES' + (true === isEng ? '_EN' : '') + '/' + language,
|
'LANGS_NAMES' + (true === isEng ? '_EN' : '') + '/' + language,
|
||||||
null,
|
null,
|
||||||
language
|
language
|
||||||
);
|
),
|
||||||
|
|
||||||
|
baseCollator = numeric => new Intl.Collator(doc.documentElement.lang, {numeric: !!numeric, sensitivity: 'base'});
|
||||||
|
|
||||||
init();
|
init();
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ export const
|
||||||
|
|
||||||
pInt = (value, defaultValue = 0) => {
|
pInt = (value, defaultValue = 0) => {
|
||||||
value = parseInt(value, 10);
|
value = parseInt(value, 10);
|
||||||
return isNaN(value) || !isFinite(value) ? defaultValue : value;
|
return isFinite(value) ? value : defaultValue;
|
||||||
},
|
},
|
||||||
|
|
||||||
defaultOptionsAfterRender = (domItem, item) =>
|
defaultOptionsAfterRender = (domItem, item) =>
|
||||||
|
|
|
||||||
|
|
@ -12,14 +12,47 @@ import { ThemeStore } from 'Stores/Theme';
|
||||||
import Remote from 'Remote/User/Fetch';
|
import Remote from 'Remote/User/Fetch';
|
||||||
import { attachmentDownload } from 'Common/Links';
|
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';
|
||||||
|
|
||||||
export const
|
export const
|
||||||
|
|
||||||
moveAction = ko.observable(false),
|
// 1 = move, 2 = copy
|
||||||
|
moveAction = ko.observable(0),
|
||||||
|
|
||||||
dropdownsDetectVisibility = (() =>
|
dropdownsDetectVisibility = (() =>
|
||||||
dropdownVisibility(!!dropdowns.find(item => item.classList.contains('show')))
|
dropdownVisibility(!!dropdowns.find(item => item.classList.contains('show')))
|
||||||
).debounce(50),
|
).debounce(50),
|
||||||
|
|
||||||
|
|
||||||
|
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))
|
||||||
|
: []
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} link
|
* @param {string} link
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
|
|
@ -89,7 +122,7 @@ computedPaginatorHelper = (koCurrentPage, koPageCount) => {
|
||||||
next = 0,
|
next = 0,
|
||||||
limit = 2;
|
limit = 2;
|
||||||
|
|
||||||
if (1 < pageCount || (0 < pageCount && pageCount < currentPage)) {
|
if (1 < pageCount) {
|
||||||
if (pageCount < currentPage) {
|
if (pageCount < currentPage) {
|
||||||
fAdd(pageCount);
|
fAdd(pageCount);
|
||||||
prev = pageCount;
|
prev = pageCount;
|
||||||
|
|
@ -260,6 +293,8 @@ viewMessage = (oMessage, popup) => {
|
||||||
class:'b-text-part'
|
class:'b-text-part'
|
||||||
+ (oMessage.pgpSigned() ? ' openpgp-signed' : '')
|
+ (oMessage.pgpSigned() ? ' openpgp-signed' : '')
|
||||||
+ (oMessage.pgpEncrypted() ? ' openpgp-encrypted' : '')
|
+ (oMessage.pgpEncrypted() ? ' openpgp-encrypted' : '')
|
||||||
|
+ (oMessage.smimeSigned() ? ' smime-signed' : '')
|
||||||
|
+ (oMessage.smimeEncrypted() ? ' smime-encrypted' : '')
|
||||||
});
|
});
|
||||||
MessageUserStore.purgeCache();
|
MessageUserStore.purgeCache();
|
||||||
}
|
}
|
||||||
|
|
@ -325,5 +360,5 @@ populateMessageBody = (oMessage, popup) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
leftPanelDisabled.subscribe(value => value && moveAction(false));
|
leftPanelDisabled.subscribe(value => value && moveAction(0));
|
||||||
moveAction.subscribe(value => value && leftPanelDisabled(false));
|
moveAction.subscribe(value => value && leftPanelDisabled(false));
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ export class CheckboxComponent {
|
||||||
: ko.observable(!!params.value);
|
: ko.observable(!!params.value);
|
||||||
|
|
||||||
this.enable = ko.isObservable(params.enable) ? params.enable
|
this.enable = ko.isObservable(params.enable) ? params.enable
|
||||||
: ko.observable(undefined === params.enable || !!params.enable);
|
: ko.observable(params.enable ?? 1);
|
||||||
|
|
||||||
this.label = params.label;
|
this.label = params.label;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { doc, createElement, addEventsListeners } from 'Common/Globals';
|
import { doc, createElement, addEventsListeners } from 'Common/Globals';
|
||||||
import { EmailModel, addressparser } from 'Model/Email';
|
import { EmailModel } from 'Model/Email';
|
||||||
|
import { addressparser } from 'Mime/Address';
|
||||||
|
|
||||||
const contentType = 'snappymail/emailaddress',
|
const contentType = 'snappymail/emailaddress',
|
||||||
getAddressKey = li => li?.emailaddress?.key,
|
getAddressKey = li => li?.emailaddress?.key,
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ export class JCard {
|
||||||
if (input) {
|
if (input) {
|
||||||
// read from jCard
|
// read from jCard
|
||||||
if (typeof input !== 'object') {
|
if (typeof input !== 'object') {
|
||||||
throw new Error('error reading vcard')
|
throw Error('error reading vcard')
|
||||||
}
|
}
|
||||||
this.parseFromJCard(input)
|
this.parseFromJCard(input)
|
||||||
}
|
}
|
||||||
|
|
@ -87,7 +87,7 @@ export class JCard {
|
||||||
arg = new VCardProperty(String(arg), value, params, type);
|
arg = new VCardProperty(String(arg), value, params, type);
|
||||||
}
|
}
|
||||||
if (!(arg instanceof VCardProperty)) {
|
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();
|
let field = arg.getField();
|
||||||
this.props.set(field, [arg]);
|
this.props.set(field, [arg]);
|
||||||
|
|
@ -101,7 +101,7 @@ export class JCard {
|
||||||
arg = new VCardProperty(String(arg), value, params, type);
|
arg = new VCardProperty(String(arg), value, params, type);
|
||||||
}
|
}
|
||||||
if (!(arg instanceof VCardProperty)) {
|
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
|
// VCardProperty arguments
|
||||||
let field = arg.getField();
|
let field = arg.getField();
|
||||||
|
|
@ -124,15 +124,15 @@ export class JCard {
|
||||||
// VCardProperty argument
|
// VCardProperty argument
|
||||||
else if (arg instanceof VCardProperty) {
|
else if (arg instanceof VCardProperty) {
|
||||||
let propArray = this.props.get(arg.getField());
|
let propArray = this.props.get(arg.getField());
|
||||||
if (!(propArray === null || propArray === void 0 ? void 0 : propArray.includes(arg)))
|
if (!propArray?.includes(arg))
|
||||||
throw new Error("Attempted to remove VCardProperty VCard does not have: ".concat(arg));
|
throw Error("Attempted to remove VCardProperty VCard does not have: ".concat(arg));
|
||||||
propArray.splice(propArray.indexOf(arg), 1);
|
propArray.splice(propArray.indexOf(arg), 1);
|
||||||
if (propArray.length === 0)
|
if (propArray.length === 0)
|
||||||
this.props.delete(arg.getField());
|
this.props.delete(arg.getField());
|
||||||
}
|
}
|
||||||
// incorrect arguments
|
// incorrect arguments
|
||||||
else
|
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');
|
'string and optional param filter or a VCardProperty');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -202,7 +202,7 @@ export class JCard {
|
||||||
parseFullName(options) {
|
parseFullName(options) {
|
||||||
let n = this.getOne('n');
|
let n = this.getOne('n');
|
||||||
if (n === undefined) {
|
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 = '';
|
let fnString = '';
|
||||||
// Position in n -> position in fn
|
// Position in n -> position in fn
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,7 @@ export class VCardProperty {
|
||||||
}
|
}
|
||||||
// invalid property
|
// invalid property
|
||||||
else {
|
else {
|
||||||
throw new Error('invalid Property constructor');
|
throw Error('invalid Property constructor');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
58
dev/External/SquireUI.js
vendored
58
dev/External/SquireUI.js
vendored
|
|
@ -133,11 +133,11 @@ class SquireUI
|
||||||
dir: {
|
dir: {
|
||||||
dir_ltr: {
|
dir_ltr: {
|
||||||
html: '⁋',
|
html: '⁋',
|
||||||
cmd: () => squire.bidi('ltr')
|
cmd: () => squire.setTextDirection('ltr')
|
||||||
},
|
},
|
||||||
dir_rtl: {
|
dir_rtl: {
|
||||||
html: '¶',
|
html: '¶',
|
||||||
cmd: () => squire.bidi('rtl')
|
cmd: () => squire.setTextDirection('rtl')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
colors: {
|
colors: {
|
||||||
|
|
@ -237,7 +237,7 @@ class SquireUI
|
||||||
cmd: () => {
|
cmd: () => {
|
||||||
let node = squire.getSelectionClosest('IMG'),
|
let node = squire.getSelectionClosest('IMG'),
|
||||||
src = prompt("Image", node?.src || "https://");
|
src = prompt("Image", node?.src || "https://");
|
||||||
src?.length ? squire.insertImage(src) : (node && squire.detach(node));
|
src?.length ? squire.insertImage(src) : node?.remove();
|
||||||
},
|
},
|
||||||
matches: 'IMG'
|
matches: 'IMG'
|
||||||
},
|
},
|
||||||
|
|
@ -270,6 +270,13 @@ class SquireUI
|
||||||
btn.classList.toggle('active', 'source' == this.mode);
|
btn.classList.toggle('active', 'source' == this.mode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
clear: {
|
||||||
|
removeStyle: {
|
||||||
|
html: '⎚',
|
||||||
|
cmd: () => squire.setStyle()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -318,11 +325,6 @@ class SquireUI
|
||||||
wysiwyg.className = 'squire-wysiwyg';
|
wysiwyg.className = 'squire-wysiwyg';
|
||||||
wysiwyg.dir = 'auto';
|
wysiwyg.dir = 'auto';
|
||||||
this.mode = ''; // 'plain' | 'wysiwyg'
|
this.mode = ''; // 'plain' | 'wysiwyg'
|
||||||
this.__plain = {
|
|
||||||
getRawData: () => this.plain.value,
|
|
||||||
setRawData: plain => this.plain.value = plain
|
|
||||||
};
|
|
||||||
|
|
||||||
this.container = container;
|
this.container = container;
|
||||||
this.squire = squire;
|
this.squire = squire;
|
||||||
this.plain = plain;
|
this.plain = plain;
|
||||||
|
|
@ -403,9 +405,9 @@ class SquireUI
|
||||||
|
|
||||||
let changes = actions.changes;
|
let changes = actions.changes;
|
||||||
changes.undo.input.disabled = changes.redo.input.disabled = true;
|
changes.undo.input.disabled = changes.redo.input.disabled = true;
|
||||||
squire.addEventListener('undoStateChange', state => {
|
squire.addEventListener('undoStateChange', e => {
|
||||||
changes.undo.input.disabled = !state.canUndo;
|
changes.undo.input.disabled = !e.detail.canUndo;
|
||||||
changes.redo.input.disabled = !state.canRedo;
|
changes.redo.input.disabled = !e.detail.canRedo;
|
||||||
});
|
});
|
||||||
|
|
||||||
actions.font.fontSize.input.selectedIndex = actions.font.fontSize.defaultValueIndex;
|
actions.font.fontSize.input.selectedIndex = actions.font.fontSize.defaultValueIndex;
|
||||||
|
|
@ -478,21 +480,21 @@ class SquireUI
|
||||||
squire.addEventListener('pathChange', e => {
|
squire.addEventListener('pathChange', e => {
|
||||||
|
|
||||||
const squireRoot = squire.getRoot();
|
const squireRoot = squire.getRoot();
|
||||||
|
let elm = e.detail.element;
|
||||||
|
|
||||||
forEachObjectValue(actions, entries => {
|
forEachObjectValue(actions, entries => {
|
||||||
forEachObjectValue(entries, cfg => {
|
forEachObjectValue(entries, cfg => {
|
||||||
// cfg.matches && cfg.input.classList.toggle('active', e.element && e.element.matches(cfg.matches));
|
// cfg.matches && cfg.input.classList.toggle('active', elm && elm.matches(cfg.matches));
|
||||||
cfg.matches && cfg.input.classList.toggle('active', e.element && e.element.closestWithin(cfg.matches, squireRoot));
|
cfg.matches && cfg.input.classList.toggle('active', elm && elm.closestWithin(cfg.matches, squireRoot));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
if (e.element) {
|
if (elm) {
|
||||||
// try to find font-family and/or font-size and set "select" elements' values
|
// try to find font-family and/or font-size and set "select" elements' values
|
||||||
|
|
||||||
let sizeSelectedIndex = actions.font.fontSize.defaultValueIndex;
|
let sizeSelectedIndex = actions.font.fontSize.defaultValueIndex;
|
||||||
let familySelectedIndex = defaultFontFamilyIndex;
|
let familySelectedIndex = defaultFontFamilyIndex;
|
||||||
|
|
||||||
let elm = e.element;
|
|
||||||
let familyFound = false;
|
let familyFound = false;
|
||||||
let sizeFound = false;
|
let sizeFound = false;
|
||||||
do {
|
do {
|
||||||
|
|
@ -524,21 +526,12 @@ class SquireUI
|
||||||
});
|
});
|
||||||
/*
|
/*
|
||||||
squire.addEventListener('cursor', e => {
|
squire.addEventListener('cursor', e => {
|
||||||
console.dir({cursor:e.range});
|
console.dir({cursor:e.detail.range});
|
||||||
});
|
});
|
||||||
squire.addEventListener('select', e => {
|
squire.addEventListener('select', e => {
|
||||||
console.dir({select:e.range});
|
console.dir({select:e.detail.range});
|
||||||
});
|
});
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// CKEditor gimmicks used by HtmlEditor
|
|
||||||
this.plugins = {
|
|
||||||
plain: true
|
|
||||||
};
|
|
||||||
this.focusManager = {
|
|
||||||
hasFocus: () => squire._isFocused,
|
|
||||||
blur: () => squire.blur()
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
doAction(name) {
|
doAction(name) {
|
||||||
|
|
@ -576,7 +569,6 @@ class SquireUI
|
||||||
this.modeSelect.selectedIndex = 'plain' == this.mode ? 1 : 0;
|
this.modeSelect.selectedIndex = 'plain' == this.mode ? 1 : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// CKeditor gimmicks used by HtmlEditor
|
|
||||||
on(type, fn) {
|
on(type, fn) {
|
||||||
if ('mode' == type) {
|
if ('mode' == type) {
|
||||||
this.onModeChange = fn;
|
this.onModeChange = fn;
|
||||||
|
|
@ -643,6 +635,18 @@ class SquireUI
|
||||||
squire.setSelection( range );
|
squire.setSelection( range );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getPlainData() {
|
||||||
|
return this.plain.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
setPlainData(text) {
|
||||||
|
this.plain.value = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
blur() {
|
||||||
|
this.squire.blur();
|
||||||
|
}
|
||||||
|
|
||||||
focus() {
|
focus() {
|
||||||
if ('plain' == this.mode) {
|
if ('plain' == this.mode) {
|
||||||
this.plain.focus();
|
this.plain.focus();
|
||||||
|
|
|
||||||
6
dev/External/User/ko.js
vendored
6
dev/External/User/ko.js
vendored
|
|
@ -1,7 +1,7 @@
|
||||||
import 'External/ko';
|
import 'External/ko';
|
||||||
import ko from 'ko';
|
import ko from 'ko';
|
||||||
import { RFC822 } from 'Common/File';
|
import { RFC822 } from 'Common/File';
|
||||||
import { HtmlEditor } from 'Common/Html';
|
import { HtmlEditor } from 'Common/HtmlEditor';
|
||||||
import { timeToNode } from 'Common/Translator';
|
import { timeToNode } from 'Common/Translator';
|
||||||
import { doc, elementById, addEventsListeners, dropdowns, leftPanelDisabled } from 'Common/Globals';
|
import { doc, elementById, addEventsListeners, dropdowns, leftPanelDisabled } from 'Common/Globals';
|
||||||
import { EmailAddressesComponent } from 'Component/EmailAddresses';
|
import { EmailAddressesComponent } from 'Component/EmailAddresses';
|
||||||
|
|
@ -91,7 +91,7 @@ Object.assign(ko.bindingHandlers, {
|
||||||
};
|
};
|
||||||
|
|
||||||
if (ko.isObservable(fValue)) {
|
if (ko.isObservable(fValue)) {
|
||||||
editor = new HtmlEditor(element, fUpdateKoValue, fOnReady, fUpdateKoValue);
|
editor = new HtmlEditor(element, fOnReady, fUpdateKoValue, fUpdateKoValue);
|
||||||
|
|
||||||
fValue.__fetchEditorValue = fUpdateKoValue;
|
fValue.__fetchEditorValue = fUpdateKoValue;
|
||||||
|
|
||||||
|
|
@ -103,7 +103,7 @@ Object.assign(ko.bindingHandlers, {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
moment: {
|
time: {
|
||||||
init: ttn,
|
init: ttn,
|
||||||
update: ttn
|
update: ttn
|
||||||
},
|
},
|
||||||
|
|
|
||||||
22
dev/External/ko.js
vendored
22
dev/External/ko.js
vendored
|
|
@ -28,6 +28,11 @@ export const
|
||||||
|
|
||||||
dispose = disposable => isFunction(disposable?.dispose) && disposable.dispose(),
|
dispose = disposable => isFunction(disposable?.dispose) && disposable.dispose(),
|
||||||
|
|
||||||
|
onEvent = (element, event, fn) => {
|
||||||
|
element.addEventListener(event, fn);
|
||||||
|
ko.utils.domNodeDisposal.addDisposeCallback(element, () => element.removeEventListener(event, fn));
|
||||||
|
},
|
||||||
|
|
||||||
onKey = (key, element, fValueAccessor, fAllBindings, model) => {
|
onKey = (key, element, fValueAccessor, fAllBindings, model) => {
|
||||||
let fn = event => {
|
let fn = event => {
|
||||||
if (key == event.key) {
|
if (key == event.key) {
|
||||||
|
|
@ -36,8 +41,7 @@ export const
|
||||||
fValueAccessor().call(model);
|
fValueAccessor().call(model);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
element.addEventListener('keydown', fn);
|
onEvent(element, 'keydown', fn);
|
||||||
ko.utils.domNodeDisposal.addDisposeCallback(element, () => element.removeEventListener('keydown', fn));
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// With this we don't need delegateRunOnDestroy
|
// With this we don't need delegateRunOnDestroy
|
||||||
|
|
@ -62,8 +66,7 @@ Object.assign(ko.bindingHandlers, {
|
||||||
},
|
},
|
||||||
update: (element, fValueAccessor) => {
|
update: (element, fValueAccessor) => {
|
||||||
let value = ko.unwrap(fValueAccessor());
|
let value = ko.unwrap(fValueAccessor());
|
||||||
value = isFunction(value) ? value() : value;
|
errorTip(element, isFunction(value) ? value() : value);
|
||||||
errorTip(element, value);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -82,6 +85,15 @@ Object.assign(ko.bindingHandlers, {
|
||||||
onKey(' ', 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());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
i18nUpdate: {
|
i18nUpdate: {
|
||||||
update: (element, fValueAccessor) => {
|
update: (element, fValueAccessor) => {
|
||||||
ko.unwrap(fValueAccessor());
|
ko.unwrap(fValueAccessor());
|
||||||
|
|
@ -94,7 +106,7 @@ Object.assign(ko.bindingHandlers, {
|
||||||
const command = fValueAccessor();
|
const command = fValueAccessor();
|
||||||
|
|
||||||
if (!command || !command.canExecute) {
|
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(
|
ko.bindingHandlers['FORM'==element.nodeName ? 'submit' : 'click'].init(
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,13 @@ function typeCast(curValue, newValue) {
|
||||||
if (null != curValue) {
|
if (null != curValue) {
|
||||||
switch (typeof curValue)
|
switch (typeof curValue)
|
||||||
{
|
{
|
||||||
case 'boolean': return 0 != newValue && !!newValue;
|
case 'boolean':
|
||||||
case 'number': return isFinite(newValue) ? parseFloat(newValue) : 0;
|
return 0 != newValue && !!newValue;
|
||||||
case 'string': return null != newValue ? '' + newValue : '';
|
case 'number':
|
||||||
|
newValue = parseFloat(newValue);
|
||||||
|
return isFinite(newValue) ? newValue : 0;
|
||||||
|
case 'string':
|
||||||
|
return null != newValue ? '' + newValue : '';
|
||||||
case 'object':
|
case 'object':
|
||||||
if (curValue.constructor.reviveFromJson) {
|
if (curValue.constructor.reviveFromJson) {
|
||||||
return curValue.constructor.reviveFromJson(newValue);
|
return curValue.constructor.reviveFromJson(newValue);
|
||||||
|
|
@ -23,7 +27,7 @@ export class AbstractModel {
|
||||||
constructor() {
|
constructor() {
|
||||||
/*
|
/*
|
||||||
if (new.target === AbstractModel) {
|
if (new.target === AbstractModel) {
|
||||||
throw new Error("Can't instantiate AbstractModel!");
|
throw Error("Can't instantiate AbstractModel!");
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
Object.defineProperty(this, 'disposables', {value: []});
|
Object.defineProperty(this, 'disposables', {value: []});
|
||||||
|
|
|
||||||
|
|
@ -116,6 +116,11 @@ export class AbstractViewSettings
|
||||||
onHide() {}
|
onHide() {}
|
||||||
viewModelDom
|
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)
|
addSetting(name, valueCb)
|
||||||
{
|
{
|
||||||
let prop = name[0].toLowerCase() + name.slice(1),
|
let prop = name[0].toLowerCase() + name.slice(1),
|
||||||
|
|
@ -131,6 +136,7 @@ export class AbstractViewSettings
|
||||||
rl.app.Remote.saveSetting(name, value,
|
rl.app.Remote.saveSetting(name, value,
|
||||||
iError => {
|
iError => {
|
||||||
this[trigger](iError ? SaveSettingStatus.Failed : SaveSettingStatus.Success);
|
this[trigger](iError ? SaveSettingStatus.Failed : SaveSettingStatus.Success);
|
||||||
|
// iError || Settings.set(name, value);
|
||||||
setTimeout(() => this[trigger](SaveSettingStatus.Idle), 1000);
|
setTimeout(() => this[trigger](SaveSettingStatus.Idle), 1000);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
@ -138,6 +144,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)
|
addSettings(names)
|
||||||
{
|
{
|
||||||
names.forEach(name => {
|
names.forEach(name => {
|
||||||
|
|
|
||||||
197
dev/Mime/Address.js
Normal file
197
dev/Mime/Address.js
Normal 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
35
dev/Mime/Encoding.js
Normal 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});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -1,17 +1,5 @@
|
||||||
//import { b64Encode } from 'Common/Utils';
|
import { decodeEncodedWords, BDecode, BEncode, QPDecode, decodeText } from 'Mime/Encoding';
|
||||||
|
import { addressparser } from 'Mime/Address';
|
||||||
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});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export function ParseMime(text)
|
export function ParseMime(text)
|
||||||
{
|
{
|
||||||
|
|
@ -27,7 +15,52 @@ export function ParseMime(text)
|
||||||
this.bodyEnd = 0;
|
this.bodyEnd = 0;
|
||||||
this.boundary = '';
|
this.boundary = '';
|
||||||
this.bodyText = '';
|
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.
|
||||||
|
};
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
@ -50,26 +83,25 @@ export function ParseMime(text)
|
||||||
get body() {
|
get body() {
|
||||||
let body = this.bodyRaw,
|
let body = this.bodyRaw,
|
||||||
charset = this.header('content-type')?.params.charset,
|
charset = this.header('content-type')?.params.charset,
|
||||||
encoding = this.headerValue('content-transfer-encoding');
|
encoding = this.headerValue('content-transfer-encoding')?.toLowerCase();
|
||||||
if ('quoted-printable' == encoding) {
|
if ('quoted-printable' == encoding) {
|
||||||
body = QPDecode(body);
|
body = QPDecode(body);
|
||||||
} else if ('base64' == encoding) {
|
} else if ('base64' == encoding) {
|
||||||
body = atob(body.replace(/\r?\n/g, ''));
|
body = BDecode(body.replace(/\r?\n/g, ''));
|
||||||
}
|
}
|
||||||
return decodeText(charset, body);
|
return decodeText(charset, body);
|
||||||
}
|
}
|
||||||
|
|
||||||
get dataUrl() {
|
get dataUrl() {
|
||||||
let body = this.bodyRaw,
|
let body = this.bodyRaw,
|
||||||
encoding = this.headerValue('content-transfer-encoding');
|
encoding = this.headerValue('content-transfer-encoding')?.toLowerCase();
|
||||||
if ('base64' == encoding) {
|
if ('base64' == encoding) {
|
||||||
body = body.replace(/\r?\n/g, '');
|
body = body.replace(/\r?\n/g, '');
|
||||||
} else {
|
} else {
|
||||||
if ('quoted-printable' == encoding) {
|
if ('quoted-printable' == encoding) {
|
||||||
body = QPDecode(body);
|
body = QPDecode(body);
|
||||||
}
|
}
|
||||||
body = btoa(body);
|
body = BEncode(body);
|
||||||
// body = b64Encode(body);
|
|
||||||
}
|
}
|
||||||
return 'data:' + this.headerValue('content-type') + ';base64,' + body;
|
return 'data:' + this.headerValue('content-type') + ';base64,' + body;
|
||||||
}
|
}
|
||||||
|
|
@ -80,7 +112,7 @@ export function ParseMime(text)
|
||||||
}
|
}
|
||||||
|
|
||||||
getByContentType(type) {
|
getByContentType(type) {
|
||||||
if (type == this.headerValue('content-type')) {
|
if (type == this.headerValue('content-type')?.toLowerCase()) {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
let i = 0, p = this.parts, part;
|
let i = 0, p = this.parts, part;
|
||||||
|
|
@ -92,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 = '') =>
|
const ParsePart = (mimePart, start_pos = 0, id = '') =>
|
||||||
{
|
{
|
||||||
let part = new MimePart,
|
let part = new MimePart,
|
||||||
|
|
@ -113,11 +148,19 @@ export function ParseMime(text)
|
||||||
[...header.matchAll(/;\s*([^;=]+)=\s*"?([^;"]+)"?/g)].forEach(param =>
|
[...header.matchAll(/;\s*([^;=]+)=\s*"?([^;"]+)"?/g)].forEach(param =>
|
||||||
params[param[1].trim().toLowerCase()] = param[2].trim()
|
params[param[1].trim().toLowerCase()] = param[2].trim()
|
||||||
);
|
);
|
||||||
// encoded-word = "=?" charset "?" encoding "?" encoded-text "?="
|
let field = match[1].trim().toLowerCase();
|
||||||
match[2] = match[2].trim().replace(/=\?([^?]+)\?(B|Q)\?(.+?)\?=/g, (m, charset, encoding, text) =>
|
if (lists.includes(field)) {
|
||||||
decodeText(charset, 'B' == encoding ? atob(text) : QPDecode(text))
|
match[2] = addressparser(match[2]);
|
||||||
);
|
} else if ('keywords' === field) {
|
||||||
headers[match[1].trim().toLowerCase()] = {
|
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],
|
value: match[2],
|
||||||
params: params
|
params: params
|
||||||
};
|
};
|
||||||
|
|
@ -132,7 +175,7 @@ export function ParseMime(text)
|
||||||
let boundary = headers['content-type']?.params.boundary;
|
let boundary = headers['content-type']?.params.boundary;
|
||||||
if (boundary) {
|
if (boundary) {
|
||||||
part.boundary = 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),
|
body = mimePart.slice(head.length),
|
||||||
bodies = body.split(regex),
|
bodies = body.split(regex),
|
||||||
pos = part.bodyStart;
|
pos = part.bodyStart;
|
||||||
|
|
|
||||||
|
|
@ -4,23 +4,34 @@ import { AttachmentModel } from 'Model/Attachment';
|
||||||
import { FileInfo } from 'Common/File';
|
import { FileInfo } from 'Common/File';
|
||||||
import { BEGIN_PGP_MESSAGE } from 'Stores/User/Pgp';
|
import { BEGIN_PGP_MESSAGE } from 'Stores/User/Pgp';
|
||||||
|
|
||||||
|
import { EmailModel } from 'Model/Email';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string data
|
* @param string data
|
||||||
* @param MessageModel message
|
* @param MessageModel message
|
||||||
*/
|
*/
|
||||||
export function MimeToMessage(data, message)
|
export function MimeToMessage(data, message)
|
||||||
{
|
{
|
||||||
let signed;
|
|
||||||
const struct = ParseMime(data);
|
const struct = ParseMime(data);
|
||||||
if (struct.headers) {
|
if (struct.headers) {
|
||||||
let html = struct.getByContentType('text/html'),
|
let html = struct.getByContentType('text/html'),
|
||||||
subject = struct.headerValue('subject');
|
subject = struct.headerValue('subject');
|
||||||
html = html ? html.body : '';
|
html = html ? html.body : '';
|
||||||
|
|
||||||
|
// Content-Type: ...; protected-headers="v1"
|
||||||
subject && message.subject(subject);
|
subject && message.subject(subject);
|
||||||
|
|
||||||
// EmailCollectionModel
|
// EmailCollectionModel
|
||||||
['from','to'].forEach(name => message[name].fromString(struct.headerValue(name)));
|
['from','to'].forEach(name => {
|
||||||
|
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 => {
|
struct.forEach(part => {
|
||||||
let cd = part.header('content-disposition'),
|
let cd = part.header('content-disposition'),
|
||||||
|
|
@ -54,12 +65,28 @@ export function MimeToMessage(data, message)
|
||||||
} else {
|
} else {
|
||||||
message.attachments.push(attachment);
|
message.attachments.push(attachment);
|
||||||
}
|
}
|
||||||
} else if ('multipart/signed' === type.value && 'application/pgp-signature' === type.params.protocol) {
|
} else if ('multipart/signed' === type.value) {
|
||||||
signed = {
|
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,
|
micAlg: type.micalg,
|
||||||
bodyPart: part.parts[0],
|
bodyPart: part,
|
||||||
sigPart: part.parts[1]
|
detached: false
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -70,10 +97,7 @@ export function MimeToMessage(data, message)
|
||||||
message.plain(data);
|
message.plain(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!signed && message.plain().includes(BEGIN_PGP_MESSAGE)) {
|
if (message.plain().includes(BEGIN_PGP_MESSAGE)) {
|
||||||
signed = true;
|
message.pgpSigned(true);
|
||||||
}
|
}
|
||||||
message.pgpSigned(signed);
|
|
||||||
|
|
||||||
// TODO: Verify instantly?
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ export class AbstractCollectionModel extends Array
|
||||||
constructor() {
|
constructor() {
|
||||||
/*
|
/*
|
||||||
if (new.target === AbstractCollectionModel) {
|
if (new.target === AbstractCollectionModel) {
|
||||||
throw new Error("Can't instantiate AbstractCollectionModel!");
|
throw Error("Can't instantiate AbstractCollectionModel!");
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
super();
|
super();
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,10 @@ export class AccountModel extends AbstractModel {
|
||||||
&& setTimeout(()=>this.fetchUnread(), (Math.ceil(Math.random() * 10)) * 3000);
|
&& setTimeout(()=>this.fetchUnread(), (Math.ceil(Math.random() * 10)) * 3000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
label() {
|
||||||
|
return this.name || IDN.toUnicode(this.email);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get INBOX unread messages
|
* Get INBOX unread messages
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -126,7 +126,10 @@ export class AttachmentModel extends AbstractModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
get download() {
|
get download() {
|
||||||
return b64EncodeJSONSafe({
|
return b64EncodeJSONSafe(this.url ? {
|
||||||
|
fileName: this.fileName,
|
||||||
|
data: this.url.replace(/^.+,/, '')
|
||||||
|
} : {
|
||||||
folder: this.folder,
|
folder: this.folder,
|
||||||
uid: this.uid,
|
uid: this.uid,
|
||||||
mimeIndex: this.mimeIndex,
|
mimeIndex: this.mimeIndex,
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { baseCollator } from 'Common/Translator';
|
||||||
import { AbstractCollectionModel } from 'Model/AbstractCollection';
|
import { AbstractCollectionModel } from 'Model/AbstractCollection';
|
||||||
import { AttachmentModel } from 'Model/Attachment';
|
import { AttachmentModel } from 'Model/Attachment';
|
||||||
|
|
||||||
|
|
@ -11,7 +12,7 @@ export class AttachmentCollectionModel extends AbstractCollectionModel
|
||||||
*/
|
*/
|
||||||
static reviveFromJson(items) {
|
static reviveFromJson(items) {
|
||||||
const attachments = super.reviveFromJson(items, attachment => AttachmentModel.reviveFromJson(attachment));
|
const attachments = super.reviveFromJson(items, attachment => AttachmentModel.reviveFromJson(attachment));
|
||||||
let collator = new Intl.Collator(undefined, {numeric: true, sensitivity: 'base'});
|
let collator = baseCollator(true);
|
||||||
attachments.sort((a, b) => {
|
attachments.sort((a, b) => {
|
||||||
if (a.isInline()) {
|
if (a.isInline()) {
|
||||||
if (!b.isInline()) {
|
if (!b.isInline()) {
|
||||||
|
|
|
||||||
|
|
@ -194,13 +194,6 @@ export class ContactModel extends AbstractModel {
|
||||||
return contact;
|
return contact;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
generateUid() {
|
|
||||||
return '' + this.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
addEmail() {
|
addEmail() {
|
||||||
// home, work
|
// home, work
|
||||||
this.email.push({
|
this.email.push({
|
||||||
|
|
|
||||||
|
|
@ -4,202 +4,6 @@ import { AbstractModel } from 'Knoin/AbstractModel';
|
||||||
|
|
||||||
'use strict';
|
'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
|
|
||||||
*/
|
|
||||||
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;
|
|
||||||
// return addresses.map(item => (item.name || item.email) ? new EmailModel(item.email, item.name) : null).filter(v => v);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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: data.email.join(' ').trim(),
|
|
||||||
name: 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class EmailModel extends AbstractModel {
|
export class EmailModel extends AbstractModel {
|
||||||
/**
|
/**
|
||||||
* @param {string=} email = ''
|
* @param {string=} email = ''
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { AbstractCollectionModel } from 'Model/AbstractCollection';
|
import { AbstractCollectionModel } from 'Model/AbstractCollection';
|
||||||
import { EmailModel, addressparser } from 'Model/Email';
|
import { EmailModel } from 'Model/Email';
|
||||||
import { forEachObjectValue } from 'Common/Utils';
|
import { forEachObjectValue } from 'Common/Utils';
|
||||||
|
import { addressparser } from 'Mime/Address';
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
|
@ -51,4 +52,24 @@ export class EmailCollectionModel extends AbstractCollectionModel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,8 @@ import { AbstractCollectionModel } from 'Model/AbstractCollection';
|
||||||
import { UNUSED_OPTION_VALUE } from 'Common/Consts';
|
import { UNUSED_OPTION_VALUE } from 'Common/Consts';
|
||||||
import { isArray, getKeyByValue, forEachObjectEntry, b64EncodeJSONSafe } from 'Common/Utils';
|
import { isArray, getKeyByValue, forEachObjectEntry, b64EncodeJSONSafe } from 'Common/Utils';
|
||||||
import { ClientSideKeyNameExpandedFolders, FolderType, FolderMetadataKeys } from 'Common/EnumsUser';
|
import { ClientSideKeyNameExpandedFolders, FolderType, FolderMetadataKeys } from 'Common/EnumsUser';
|
||||||
import { clearCache, getFolderFromCacheList, setFolder, setFolderInboxName, removeFolderFromCacheList } from 'Common/Cache';
|
import { clearCache, getFolderFromCacheList, setFolder, setFolderInboxName } from 'Common/Cache';
|
||||||
import { Settings, SettingsGet, fireEvent } from 'Common/Globals';
|
import { Settings, SettingsGet, fireEvent } from 'Common/Globals';
|
||||||
import { Notifications } from 'Common/Enums';
|
|
||||||
|
|
||||||
import * as Local from 'Storage/Client';
|
import * as Local from 'Storage/Client';
|
||||||
|
|
||||||
|
|
@ -15,18 +14,23 @@ import { MessagelistUserStore } from 'Stores/User/Messagelist';
|
||||||
import { SettingsUserStore } from 'Stores/User/Settings';
|
import { SettingsUserStore } from 'Stores/User/Settings';
|
||||||
|
|
||||||
import { sortFolders } from 'Common/Folders';
|
import { sortFolders } from 'Common/Folders';
|
||||||
import { i18n, translateTrigger, getNotification } from 'Common/Translator';
|
import { i18n, translateTrigger } from 'Common/Translator';
|
||||||
|
|
||||||
import { AbstractModel } from 'Knoin/AbstractModel';
|
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 Remote from 'Remote/User/Fetch';
|
||||||
|
|
||||||
import { FileInfo } from 'Common/File';
|
import { FileInfo } from 'Common/File';
|
||||||
|
|
||||||
|
import { FolderPopupView } from 'View/Popup/Folder';
|
||||||
|
import { showScreenPopup } from 'Knoin/Knoin';
|
||||||
|
|
||||||
|
import { isAllowedKeyword } from 'Stores/User/Folder';
|
||||||
|
|
||||||
const
|
const
|
||||||
// isPosNumeric = value => null != value && /^[0-9]*$/.test(value.toString()),
|
// isPosNumeric = value => null != value && /^[0-9]*$/.test(value.toString()),
|
||||||
|
|
||||||
|
|
@ -113,6 +117,8 @@ export class FolderCollectionModel extends AbstractCollectionModel
|
||||||
this.namespace;
|
this.namespace;
|
||||||
this.optimized
|
this.optimized
|
||||||
this.capabilities
|
this.capabilities
|
||||||
|
this.allow; // allow adding
|
||||||
|
// this.exist;
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
@ -254,6 +260,10 @@ export class FolderCollectionModel extends AbstractCollectionModel
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
visible() {
|
||||||
|
return this.filter(folder => folder.visible());
|
||||||
|
}
|
||||||
|
|
||||||
storeIt() {
|
storeIt() {
|
||||||
FolderUserStore.displaySpecSetting(Settings.app('folderSpecLimit') < this.CountRec);
|
FolderUserStore.displaySpecSetting(Settings.app('folderSpecLimit') < this.CountRec);
|
||||||
|
|
||||||
|
|
@ -296,6 +306,7 @@ export class FolderModel extends AbstractModel {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.fullName = '';
|
this.fullName = '';
|
||||||
|
this.parentName = '';
|
||||||
this.delimiter = '';
|
this.delimiter = '';
|
||||||
this.deep = 0;
|
this.deep = 0;
|
||||||
this.expires = 0;
|
this.expires = 0;
|
||||||
|
|
@ -316,12 +327,10 @@ export class FolderModel extends AbstractModel {
|
||||||
|
|
||||||
focused: false,
|
focused: false,
|
||||||
selected: false,
|
selected: false,
|
||||||
editing: false,
|
|
||||||
isSubscribed: true,
|
isSubscribed: true,
|
||||||
checkable: false, // Check for new messages
|
checkable: false, // Check for new messages
|
||||||
askDelete: false,
|
askDelete: false,
|
||||||
|
|
||||||
nameForEdit: '',
|
|
||||||
errorMsg: '',
|
errorMsg: '',
|
||||||
|
|
||||||
totalEmails: 0,
|
totalEmails: 0,
|
||||||
|
|
@ -341,7 +350,6 @@ export class FolderModel extends AbstractModel {
|
||||||
this.addSubscribables({
|
this.addSubscribables({
|
||||||
kolabType: sValue => this.metadata[FolderMetadataKeys.KolabFolderType] = sValue,
|
kolabType: sValue => this.metadata[FolderMetadataKeys.KolabFolderType] = sValue,
|
||||||
permanentFlags: aValue => this.tagsAllowed(aValue.includes('\\*')),
|
permanentFlags: aValue => this.tagsAllowed(aValue.includes('\\*')),
|
||||||
editing: value => value && this.nameForEdit(this.name()),
|
|
||||||
unreadEmails: unread => FolderType.Inbox === this.type() && fireEvent('mailbox.inbox-unread-count', unread)
|
unreadEmails: unread => FolderType.Inbox === this.type() && fireEvent('mailbox.inbox-unread-count', unread)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -363,20 +371,19 @@ export class FolderModel extends AbstractModel {
|
||||||
.extend({ notify: 'always' });
|
.extend({ notify: 'always' });
|
||||||
*/
|
*/
|
||||||
/*
|
/*
|
||||||
https://www.rfc-editor.org/rfc/rfc8621.html#section-2
|
// https://www.rfc-editor.org/rfc/rfc8621.html#section-2
|
||||||
"myRights": {
|
this.myRights = {
|
||||||
"mayAddItems": true,
|
'mayAddItems': true,
|
||||||
"mayRename": false,
|
'mayCreateChild': true,
|
||||||
"maySubmit": true,
|
'mayDelete': true,
|
||||||
"mayDelete": false,
|
'mayReadItems': true,
|
||||||
"maySetKeywords": true,
|
'mayRemoveItems': true,
|
||||||
"mayRemoveItems": true,
|
'mayRename': true,
|
||||||
"mayCreateChild": true,
|
'maySetKeywords': true,
|
||||||
"maySetSeen": true,
|
'maySetSeen': true,
|
||||||
"mayReadItems": true
|
'maySubmit': true
|
||||||
},
|
};
|
||||||
*/
|
*/
|
||||||
|
|
||||||
this.addComputables({
|
this.addComputables({
|
||||||
|
|
||||||
isInbox: () => FolderType.Inbox === this.type(),
|
isInbox: () => FolderType.Inbox === this.type(),
|
||||||
|
|
@ -387,6 +394,7 @@ export class FolderModel extends AbstractModel {
|
||||||
// isSubscribed: () => this.attributes().includes('\\subscribed'),
|
// isSubscribed: () => this.attributes().includes('\\subscribed'),
|
||||||
|
|
||||||
hasVisibleSubfolders: () => !!this.subFolders().find(folder => folder.visible()),
|
hasVisibleSubfolders: () => !!this.subFolders().find(folder => folder.visible()),
|
||||||
|
visibleSubfolders: () => this.subFolders().visible(),
|
||||||
|
|
||||||
hasSubscriptions: () => this.isSubscribed() | !!this.subFolders().find(
|
hasSubscriptions: () => this.isSubscribed() | !!this.subFolders().find(
|
||||||
oFolder => {
|
oFolder => {
|
||||||
|
|
@ -395,8 +403,6 @@ export class FolderModel extends AbstractModel {
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
|
||||||
canBeEdited: () => !this.type() && this.exists/* && this.selectable()*/,
|
|
||||||
|
|
||||||
isSystemFolder: () => this.type()
|
isSystemFolder: () => this.type()
|
||||||
| (FolderUserStore.allowKolab() && !!this.kolabType() & !SettingsUserStore.unhideKolabFolders()),
|
| (FolderUserStore.allowKolab() && !!this.kolabType() & !SettingsUserStore.unhideKolabFolders()),
|
||||||
|
|
||||||
|
|
@ -407,6 +413,8 @@ export class FolderModel extends AbstractModel {
|
||||||
canBeSubscribed: () => this.selectable()
|
canBeSubscribed: () => this.selectable()
|
||||||
&& !(this.isSystemFolder() | !SettingsUserStore.hideUnsubscribed()),
|
&& !(this.isSystemFolder() | !SettingsUserStore.hideUnsubscribed()),
|
||||||
|
|
||||||
|
optionalTags: () => this.permanentFlags.filter(isAllowedKeyword),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Folder is visible when:
|
* Folder is visible when:
|
||||||
* - hasVisibleSubfolders()
|
* - hasVisibleSubfolders()
|
||||||
|
|
@ -463,60 +471,30 @@ export class FolderModel extends AbstractModel {
|
||||||
|
|
||||||
detailedName: () => this.name() + ' ' + this.nameInfo(),
|
detailedName: () => this.name() + ' ' + this.nameInfo(),
|
||||||
|
|
||||||
hasSubscribedUnreadMessagesSubfolders: () =>
|
icon: () => {
|
||||||
!!this.subFolders().find(
|
switch (this.type())
|
||||||
folder => folder.unreadCount() | folder.hasSubscribedUnreadMessagesSubfolders()
|
{
|
||||||
)
|
case 1: return '📥'; // FolderType.Inbox
|
||||||
/*
|
case 2: return '📧'; // FolderType.Sent icon-paper-plane
|
||||||
!!this.subFolders().filter(
|
case 3: return '🗎'; // FolderType.Drafts
|
||||||
folder => folder.unreadCount() | folder.hasSubscribedUnreadMessagesSubfolders()
|
case 4: return '⚠'; // FolderType.Junk
|
||||||
).length
|
case 5: return '🗑'; // FolderType.Trash
|
||||||
*/
|
case 6: return '🗄'; // FolderType.Archive
|
||||||
// ,href: () => this.canBeSelected() && mailBox(this.fullNameHash)
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
|
||||||
|
hasUnreadInSub: () =>
|
||||||
|
this.subFolders().some(
|
||||||
|
folder => folder.unreadEmails() | folder.hasUnreadInSub()
|
||||||
|
),
|
||||||
|
|
||||||
|
href: () => this.canBeSelected() && mailBox(this.fullNameHash)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
edit() {
|
edit() {
|
||||||
this.canBeEdited() && this.editing(true);
|
showScreenPopup(FolderPopupView, [this]);
|
||||||
}
|
|
||||||
|
|
||||||
unedit() {
|
|
||||||
this.editing(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
rename() {
|
|
||||||
const folder = this,
|
|
||||||
nameToEdit = folder.nameForEdit().trim();
|
|
||||||
if (nameToEdit && folder.name() !== nameToEdit) {
|
|
||||||
Remote.abort('Folders').post('FolderRename', FolderUserStore.foldersRenaming, {
|
|
||||||
folder: folder.fullName,
|
|
||||||
newFolderName: nameToEdit,
|
|
||||||
subscribe: folder.isSubscribed() ? 1 : 0
|
|
||||||
})
|
|
||||||
.then(data => {
|
|
||||||
folder.name(nameToEdit/*data.name*/);
|
|
||||||
if (folder.subFolders.length) {
|
|
||||||
Remote.setTrigger(FolderUserStore.foldersLoading, true);
|
|
||||||
// clearTimeout(Remote.foldersTimeout);
|
|
||||||
// Remote.foldersTimeout = setTimeout(loadFolders, 500);
|
|
||||||
setTimeout(loadFolders, 500);
|
|
||||||
// TODO: rename all subfolders with folder.delimiter to prevent reload?
|
|
||||||
} else {
|
|
||||||
removeFolderFromCacheList(folder.fullName);
|
|
||||||
folder.fullName = data.Result.fullName;
|
|
||||||
setFolder(folder);
|
|
||||||
const parent = getFolderFromCacheList(folder.parentName);
|
|
||||||
sortFolders(parent ? parent.subFolders : FolderUserStore.folderList);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
FolderUserStore.error(
|
|
||||||
getNotification(error.code, '', Notifications.CantRenameFolder)
|
|
||||||
+ '.\n' + error.message);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
folder.editing(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { AbstractModel } from 'Knoin/AbstractModel';
|
import { AbstractModel } from 'Knoin/AbstractModel';
|
||||||
import { addObservablesTo } from 'External/ko';
|
import { addObservablesTo, addComputablesTo } from 'External/ko';
|
||||||
|
|
||||||
export class IdentityModel extends AbstractModel {
|
export class IdentityModel extends AbstractModel {
|
||||||
/**
|
/**
|
||||||
|
|
@ -11,17 +11,31 @@ export class IdentityModel extends AbstractModel {
|
||||||
|
|
||||||
addObservablesTo(this, {
|
addObservablesTo(this, {
|
||||||
id: '',
|
id: '',
|
||||||
|
label: '',
|
||||||
email: '',
|
email: '',
|
||||||
name: '',
|
name: '',
|
||||||
|
|
||||||
replyTo: '',
|
replyTo: '',
|
||||||
bcc: '',
|
bcc: '',
|
||||||
|
sentFolder: '',
|
||||||
|
|
||||||
signature: '',
|
signature: '',
|
||||||
signatureInsertBefore: false,
|
signatureInsertBefore: false,
|
||||||
|
|
||||||
|
pgpSign: false,
|
||||||
|
pgpEncrypt: false,
|
||||||
|
|
||||||
|
smimeKey: '',
|
||||||
|
smimeCertificate: '',
|
||||||
|
|
||||||
askDelete: false
|
askDelete: 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())
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -29,8 +43,8 @@ export class IdentityModel extends AbstractModel {
|
||||||
*/
|
*/
|
||||||
formattedName() {
|
formattedName() {
|
||||||
const name = this.name(),
|
const name = this.name(),
|
||||||
email = this.email();
|
email = this.email(),
|
||||||
|
label = this.label();
|
||||||
return name ? name + ' <' + email + '>' : email;
|
return (name ? `${name} ` : '') + `<${email}>` + (label ? ` (${label})` : '');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,12 +8,14 @@ import { forEachObjectEntry, b64EncodeJSONSafe } from 'Common/Utils';
|
||||||
import { serverRequestRaw, proxy } from 'Common/Links';
|
import { serverRequestRaw, proxy } from 'Common/Links';
|
||||||
import { addObservablesTo, addComputablesTo } from 'External/ko';
|
import { addObservablesTo, addComputablesTo } from 'External/ko';
|
||||||
|
|
||||||
import { FolderUserStore, isAllowedKeyword } from 'Stores/User/Folder';
|
import { FolderUserStore } from 'Stores/User/Folder';
|
||||||
import { SettingsUserStore } from 'Stores/User/Settings';
|
import { SettingsUserStore } from 'Stores/User/Settings';
|
||||||
|
|
||||||
import { FileInfo, RFC822 } from 'Common/File';
|
import { FileInfo, RFC822 } from 'Common/File';
|
||||||
import { AttachmentCollectionModel } from 'Model/AttachmentCollection';
|
import { AttachmentCollectionModel } from 'Model/AttachmentCollection';
|
||||||
import { EmailCollectionModel } from 'Model/EmailCollection';
|
import { EmailCollectionModel } from 'Model/EmailCollection';
|
||||||
|
import { MimeHeaderCollectionModel } from 'Model/MimeHeaderCollection';
|
||||||
|
//import { MimeHeaderAutocryptModel } from 'Model/MimeHeaderAutocrypt';
|
||||||
import { AbstractModel } from 'Knoin/AbstractModel';
|
import { AbstractModel } from 'Knoin/AbstractModel';
|
||||||
|
|
||||||
import PreviewHTML from 'Html/PreviewMessage.html';
|
import PreviewHTML from 'Html/PreviewMessage.html';
|
||||||
|
|
@ -22,6 +24,8 @@ import { LanguageStore } from 'Stores/Language';
|
||||||
|
|
||||||
import Remote from 'Remote/User/Fetch';
|
import Remote from 'Remote/User/Fetch';
|
||||||
|
|
||||||
|
import { MimeToMessage } from 'Mime/Utils';
|
||||||
|
|
||||||
const
|
const
|
||||||
msgHtml = msg => cleanHtml(msg.html(), msg.attachments(), '#rl-msg-' + msg.hash),
|
msgHtml = msg => cleanHtml(msg.html(), msg.attachments(), '#rl-msg-' + msg.hash),
|
||||||
|
|
||||||
|
|
@ -55,42 +59,50 @@ export class MessageModel extends AbstractModel {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.folder = '';
|
Object.assign(this, {
|
||||||
this.uid = 0;
|
folder: '',
|
||||||
this.hash = '';
|
uid: 0,
|
||||||
this.from = new EmailCollectionModel;
|
hash: '',
|
||||||
this.to = new EmailCollectionModel;
|
from: new EmailCollectionModel,
|
||||||
this.cc = new EmailCollectionModel;
|
to: new EmailCollectionModel,
|
||||||
this.bcc = new EmailCollectionModel;
|
cc: new EmailCollectionModel,
|
||||||
this.sender = new EmailCollectionModel;
|
bcc: new EmailCollectionModel,
|
||||||
this.replyTo = new EmailCollectionModel;
|
sender: new EmailCollectionModel,
|
||||||
this.deliveredTo = new EmailCollectionModel;
|
replyTo: new EmailCollectionModel,
|
||||||
this.body = null;
|
deliveredTo: new EmailCollectionModel,
|
||||||
this.draftInfo = [];
|
body: null,
|
||||||
this.dkim = [];
|
draftInfo: [],
|
||||||
this.spf = [];
|
dkim: [],
|
||||||
this.dmarc = [];
|
spf: [],
|
||||||
this.messageId = '';
|
dmarc: [],
|
||||||
this.inReplyTo = '';
|
messageId: '',
|
||||||
this.references = '';
|
inReplyTo: '',
|
||||||
this.autocrypt = {};
|
references: '',
|
||||||
|
// autocrypt: ko.observableArray(),
|
||||||
|
hasVirus: null, // or boolean when scanned
|
||||||
|
priority: 3, // Normal
|
||||||
|
senderEmailsString: '',
|
||||||
|
senderClearEmailsString: '',
|
||||||
|
isSpam: false,
|
||||||
|
spamScore: 0,
|
||||||
|
spamResult: '',
|
||||||
|
size: 0,
|
||||||
|
readReceipt: '',
|
||||||
|
preview: null,
|
||||||
|
|
||||||
|
attachments: ko.observableArray(new AttachmentCollectionModel),
|
||||||
|
threads: ko.observableArray(),
|
||||||
|
threadUnseen: ko.observableArray(),
|
||||||
|
unsubsribeLinks: ko.observableArray(),
|
||||||
|
flags: ko.observableArray(),
|
||||||
|
headers: ko.observableArray(new MimeHeaderCollectionModel)
|
||||||
|
});
|
||||||
|
|
||||||
addObservablesTo(this, {
|
addObservablesTo(this, {
|
||||||
subject: '',
|
subject: '',
|
||||||
plain: '',
|
plain: '',
|
||||||
html: '',
|
html: '',
|
||||||
preview: null,
|
|
||||||
size: 0,
|
|
||||||
spamScore: 0,
|
|
||||||
spamResult: '',
|
|
||||||
isSpam: false,
|
|
||||||
hasVirus: null, // or boolean when scanned
|
|
||||||
dateTimestamp: 0,
|
dateTimestamp: 0,
|
||||||
internalTimestamp: 0,
|
|
||||||
priority: 3, // Normal
|
|
||||||
|
|
||||||
senderEmailsString: '',
|
|
||||||
senderClearEmailsString: '',
|
|
||||||
|
|
||||||
deleted: false,
|
deleted: false,
|
||||||
|
|
||||||
|
|
@ -102,26 +114,29 @@ export class MessageModel extends AbstractModel {
|
||||||
isHtml: false,
|
isHtml: false,
|
||||||
hasImages: false,
|
hasImages: false,
|
||||||
hasExternals: false,
|
hasExternals: false,
|
||||||
|
hasTracking: false,
|
||||||
pgpSigned: null,
|
|
||||||
pgpVerified: null,
|
|
||||||
|
|
||||||
encrypted: false,
|
encrypted: false,
|
||||||
|
|
||||||
|
pgpSigned: null,
|
||||||
pgpEncrypted: null,
|
pgpEncrypted: null,
|
||||||
pgpDecrypted: false,
|
pgpDecrypted: false,
|
||||||
|
|
||||||
readReceipt: '',
|
smimeSigned: null,
|
||||||
|
smimeEncrypted: null,
|
||||||
|
smimeDecrypted: false,
|
||||||
|
|
||||||
// rfc8621
|
// rfc8621
|
||||||
id: '',
|
id: '',
|
||||||
// threadId: ''
|
// threadId: ''
|
||||||
});
|
|
||||||
|
|
||||||
this.attachments = ko.observableArray(new AttachmentCollectionModel);
|
/**
|
||||||
this.threads = ko.observableArray();
|
* Basic support for Linked Data (Structured Email)
|
||||||
this.threadUnseen = ko.observableArray();
|
* https://json-ld.org/
|
||||||
this.unsubsribeLinks = ko.observableArray();
|
* https://structured.email/
|
||||||
this.flags = ko.observableArray();
|
**/
|
||||||
|
linkedData: []
|
||||||
|
});
|
||||||
|
|
||||||
addComputablesTo(this, {
|
addComputablesTo(this, {
|
||||||
attachmentIconClass: () =>
|
attachmentIconClass: () =>
|
||||||
|
|
@ -141,17 +156,15 @@ export class MessageModel extends AbstractModel {
|
||||||
|
|
||||||
tagOptions: () => {
|
tagOptions: () => {
|
||||||
const tagOptions = [];
|
const tagOptions = [];
|
||||||
FolderUserStore.currentFolder().permanentFlags.forEach(value => {
|
FolderUserStore.currentFolder().optionalTags().forEach(value => {
|
||||||
if (isAllowedKeyword(value)) {
|
let lower = value.toLowerCase();
|
||||||
let lower = value.toLowerCase();
|
tagOptions.push({
|
||||||
tagOptions.push({
|
css: 'msgflag-' + lower,
|
||||||
css: 'msgflag-' + lower,
|
value: value,
|
||||||
value: value,
|
checked: this.flags().includes(lower),
|
||||||
checked: this.flags().includes(lower),
|
label: i18n('MESSAGE_TAGS/'+lower, 0, value),
|
||||||
label: i18n('MESSAGE_TAGS/'+lower, 0, value),
|
toggle: (/*obj*/) => toggleTag(this, value)
|
||||||
toggle: (/*obj*/) => toggleTag(this, value)
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
return tagOptions
|
return tagOptions
|
||||||
},
|
},
|
||||||
|
|
@ -180,6 +193,10 @@ export class MessageModel extends AbstractModel {
|
||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.smimeSigned.subscribe(value =>
|
||||||
|
value?.body && MimeToMessage(value.body, this)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
get requestHash() {
|
get requestHash() {
|
||||||
|
|
@ -197,23 +214,23 @@ export class MessageModel extends AbstractModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
spamStatus() {
|
spamStatus() {
|
||||||
let spam = this.spamResult();
|
let spam = this.spamResult;
|
||||||
return spam ? i18n(this.isSpam() ? 'GLOBAL/SPAM' : 'GLOBAL/NOT_SPAM') + ': ' + spam : '';
|
return spam ? i18n(this.isSpam ? 'GLOBAL/SPAM' : 'GLOBAL/NOT_SPAM') + ': ' + spam : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
friendlySize() {
|
friendlySize() {
|
||||||
return FileInfo.friendlySize(this.size());
|
return FileInfo.friendlySize(this.size);
|
||||||
}
|
}
|
||||||
|
|
||||||
computeSenderEmail() {
|
computeSenderEmail() {
|
||||||
const list = this[
|
const list = this[
|
||||||
[FolderUserStore.sentFolder(), FolderUserStore.draftsFolder()].includes(this.folder) ? 'to' : 'from'
|
[FolderUserStore.sentFolder(), FolderUserStore.draftsFolder()].includes(this.folder) ? 'to' : 'from'
|
||||||
];
|
];
|
||||||
this.senderEmailsString(list.toString(true));
|
this.senderEmailsString = list.toString(true);
|
||||||
this.senderClearEmailsString(list.map(email => email?.email).filter(email => email).join(', '));
|
this.senderClearEmailsString = list.map(email => email?.email).filter(email => email).join(', ');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -224,8 +241,63 @@ export class MessageModel extends AbstractModel {
|
||||||
if (super.revivePropertiesFromJson(json)) {
|
if (super.revivePropertiesFromJson(json)) {
|
||||||
// this.foundCIDs = isArray(json.FoundCIDs) ? json.FoundCIDs : [];
|
// this.foundCIDs = isArray(json.FoundCIDs) ? json.FoundCIDs : [];
|
||||||
// this.attachments(AttachmentCollectionModel.reviveFromJson(json.attachments, this.foundCIDs));
|
// this.attachments(AttachmentCollectionModel.reviveFromJson(json.attachments, this.foundCIDs));
|
||||||
|
// this.headers(MimeHeaderCollectionModel.reviveFromJson(json.headers));
|
||||||
|
|
||||||
this.computeSenderEmail();
|
this.computeSenderEmail();
|
||||||
|
|
||||||
|
let value, headers = this.headers();
|
||||||
|
/* // These could be by Envelope or MIME
|
||||||
|
this.messageId = headers.valueByName('Message-Id');
|
||||||
|
this.subject(headers.valueByName('Subject'));
|
||||||
|
this.sender = EmailCollectionModel.fromString(headers.valueByName('Sender'));
|
||||||
|
this.from = EmailCollectionModel.fromArray(headers.valueByName('From'));
|
||||||
|
this.replyTo = EmailCollectionModel.fromArray(headers.valueByName('Reply-To'));
|
||||||
|
this.to = EmailCollectionModel.fromArray(headers.valueByName('To'));
|
||||||
|
this.cc = EmailCollectionModel.fromArray(headers.valueByName('Cc'));
|
||||||
|
this.bcc = EmailCollectionModel.fromArray(headers.valueByName('Bcc'));
|
||||||
|
this.inReplyTo = headers.valueByName('In-Reply-To');
|
||||||
|
|
||||||
|
this.deliveredTo = EmailCollectionModel.fromString(headers.valueByName('Delivered-To'));
|
||||||
|
*/
|
||||||
|
// Priority
|
||||||
|
value = headers.valueByName('X-MSMail-Priority')
|
||||||
|
|| headers.valueByName('Importance')
|
||||||
|
|| headers.valueByName('X-Priority');
|
||||||
|
if (value) {
|
||||||
|
if (/[h12]/.test(value[0])) {
|
||||||
|
this.priority = 1;
|
||||||
|
} else if (/[l45]/.test(value[0])) {
|
||||||
|
this.priority = 5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unsubscribe links
|
||||||
|
if (value = headers.valueByName('List-Unsubscribe')) {
|
||||||
|
this.unsubsribeLinks(value.split(',').map(
|
||||||
|
link => link.replace(/^[ <>]+|[ <>]+$/g, '')
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (headers.valueByName('X-Virus')) {
|
||||||
|
this.hasVirus = true;
|
||||||
|
}
|
||||||
|
if (value = headers.valueByName('X-Virus-Status')) {
|
||||||
|
if (value.includes('infected')) {
|
||||||
|
this.hasVirus = true;
|
||||||
|
} else if (value.includes('clean')) {
|
||||||
|
this.hasVirus = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
if (value = headers.valueByName('X-Virus-Scanned')) {
|
||||||
|
this.virusScanned(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://autocrypt.org/level1.html#the-autocrypt-header
|
||||||
|
headers.valuesByName('Autocrypt').forEach(value => {
|
||||||
|
this.autocrypt.push(new MimeHeaderAutocryptModel(value));
|
||||||
|
});
|
||||||
|
*/
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -241,7 +313,7 @@ export class MessageModel extends AbstractModel {
|
||||||
checked: this.checked(),
|
checked: this.checked(),
|
||||||
unseen: this.isUnseen(),
|
unseen: this.isUnseen(),
|
||||||
focused: this.focused(),
|
focused: this.focused(),
|
||||||
priorityHigh: this.priority() === 1,
|
priorityHigh: this.priority === 1,
|
||||||
withAttachments: !!this.attachments().length,
|
withAttachments: !!this.attachments().length,
|
||||||
// hasChildrenMessage: 1 < this.threadsLen()
|
// hasChildrenMessage: 1 < this.threadsLen()
|
||||||
}, (key, value) => value && classes.push(key));
|
}, (key, value) => value && classes.push(key));
|
||||||
|
|
@ -307,8 +379,10 @@ export class MessageModel extends AbstractModel {
|
||||||
let result = msgHtml(this);
|
let result = msgHtml(this);
|
||||||
this.hasExternals(result.hasExternals);
|
this.hasExternals(result.hasExternals);
|
||||||
this.hasImages(!!result.hasExternals);
|
this.hasImages(!!result.hasExternals);
|
||||||
|
this.hasTracking(!!result.tracking);
|
||||||
|
this.linkedData(result.linkedData);
|
||||||
body.innerHTML = result.html;
|
body.innerHTML = result.html;
|
||||||
if (!this.isSpam() && FolderUserStore.spamFolder() != this.folder) {
|
if (!this.isSpam && FolderUserStore.spamFolder() != this.folder) {
|
||||||
if ('always' === SettingsUserStore.viewImages()) {
|
if ('always' === SettingsUserStore.viewImages()) {
|
||||||
this.showExternalImages();
|
this.showExternalImages();
|
||||||
}
|
}
|
||||||
|
|
@ -343,6 +417,11 @@ export class MessageModel extends AbstractModel {
|
||||||
return this.viewBody(false);
|
return this.viewBody(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
swapColors() {
|
||||||
|
const cl = this.body?.classList;
|
||||||
|
cl && cl.toggle('swapColors');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {boolean=} print = false
|
* @param {boolean=} print = false
|
||||||
*/
|
*/
|
||||||
|
|
@ -381,13 +460,6 @@ export class MessageModel extends AbstractModel {
|
||||||
this.popupMessage(true);
|
this.popupMessage(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
generateUid() {
|
|
||||||
return this.folder + '/' + this.uid;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns {MessageModel}
|
* @returns {MessageModel}
|
||||||
*//*
|
*//*
|
||||||
|
|
@ -438,7 +510,7 @@ export class MessageModel extends AbstractModel {
|
||||||
hasImages = true;
|
hasImages = true;
|
||||||
},
|
},
|
||||||
attr = 'data-x-src',
|
attr = 'data-x-src',
|
||||||
src, useProxy = !!SettingsGet('useLocalProxyForExternalImages');
|
src, useProxy = !!SettingsGet('proxyExternalImages');
|
||||||
body.querySelectorAll('img[' + attr + ']').forEach(node => {
|
body.querySelectorAll('img[' + attr + ']').forEach(node => {
|
||||||
src = node.getAttribute(attr);
|
src = node.getAttribute(attr);
|
||||||
if (isValid(src)) {
|
if (isValid(src)) {
|
||||||
|
|
|
||||||
13
dev/Model/MimeHeader.js
Normal file
13
dev/Model/MimeHeader.js
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
32
dev/Model/MimeHeaderAutocrypt.js
Normal file
32
dev/Model/MimeHeaderAutocrypt.js
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
//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.split('=');
|
||||||
|
const trim = str => (str || '').trim().replace(/^["']|["']+$/g, '');
|
||||||
|
this[trim(entry[0]).replace('-', '_')] = trim(entry[1]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toString() {
|
||||||
|
if ('mutual' === this.prefer_encrypt) {
|
||||||
|
return `addr=${this.addr}; prefer-encrypt=mutual; keydata=${this.keydata}`;
|
||||||
|
}
|
||||||
|
return `addr=${this.addr}; keydata=${this.keydata}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
pem() {
|
||||||
|
return '-----BEGIN PGP PUBLIC KEY BLOCK-----\n\n'
|
||||||
|
+ this.keydata
|
||||||
|
+ '\n-----END PGP PUBLIC KEY BLOCK-----';
|
||||||
|
}
|
||||||
|
}
|
||||||
38
dev/Model/MimeHeaderCollection.js
Normal file
38
dev/Model/MimeHeaderCollection.js
Normal 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -19,7 +19,6 @@ checkResponseError = data => {
|
||||||
Notifications.DomainNotAllowed,
|
Notifications.DomainNotAllowed,
|
||||||
Notifications.AccountNotAllowed,
|
Notifications.AccountNotAllowed,
|
||||||
Notifications.MailServerError,
|
Notifications.MailServerError,
|
||||||
Notifications.UnknownNotification,
|
|
||||||
Notifications.UnknownError
|
Notifications.UnknownError
|
||||||
].includes(err)
|
].includes(err)
|
||||||
) {
|
) {
|
||||||
|
|
@ -132,8 +131,8 @@ export class AbstractFetchRemote
|
||||||
|
|
||||||
fetchJSON(sAction, getURL(sGetAdd),
|
fetchJSON(sAction, getURL(sGetAdd),
|
||||||
sGetAdd ? null : (params || {}),
|
sGetAdd ? null : (params || {}),
|
||||||
undefined === iTimeout ? 30000 : pInt(iTimeout),
|
pInt(iTimeout ?? 30000),
|
||||||
data => {
|
async data => {
|
||||||
let iError = 0;
|
let iError = 0;
|
||||||
if (data) {
|
if (data) {
|
||||||
/*
|
/*
|
||||||
|
|
@ -149,6 +148,10 @@ export class AbstractFetchRemote
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (111 === iError && rl.app.ask && await rl.app.ask.cryptkey()) {
|
||||||
|
return this.request(sAction, fCallback, params, iTimeout, sGetAdd);
|
||||||
|
}
|
||||||
|
|
||||||
fCallback && fCallback(
|
fCallback && fCallback(
|
||||||
iError,
|
iError,
|
||||||
data,
|
data,
|
||||||
|
|
@ -170,13 +173,6 @@ export class AbstractFetchRemote
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {?Function} fCallback
|
|
||||||
*/
|
|
||||||
getPublicKey(fCallback) {
|
|
||||||
this.request('GetPublicKey', fCallback);
|
|
||||||
}
|
|
||||||
|
|
||||||
setTrigger(trigger, value) {
|
setTrigger(trigger, value) {
|
||||||
if (trigger) {
|
if (trigger) {
|
||||||
value = !!value;
|
value = !!value;
|
||||||
|
|
@ -193,12 +189,16 @@ export class AbstractFetchRemote
|
||||||
post(action, fTrigger, params, timeOut) {
|
post(action, fTrigger, params, timeOut) {
|
||||||
this.setTrigger(fTrigger, true);
|
this.setTrigger(fTrigger, true);
|
||||||
return fetchJSON(action, getURL(), params || {}, pInt(timeOut, 30000),
|
return fetchJSON(action, getURL(), params || {}, pInt(timeOut, 30000),
|
||||||
data => {
|
async data => {
|
||||||
abort(action, 0, 1);
|
abort(action, 0, 1);
|
||||||
|
|
||||||
if (!data) {
|
if (!data) {
|
||||||
return Promise.reject(new FetchError(Notifications.JsonParse));
|
return Promise.reject(new FetchError(Notifications.JsonParse));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (111 === data?.ErrorCode && rl.app.ask && await rl.app.ask.cryptkey()) {
|
||||||
|
return this.post(action, fTrigger, params, timeOut);
|
||||||
|
}
|
||||||
/*
|
/*
|
||||||
let isCached = false, type = '';
|
let isCached = false, type = '';
|
||||||
if (data?.epoch) {
|
if (data?.epoch) {
|
||||||
|
|
|
||||||
|
|
@ -64,16 +64,6 @@ class RemoteUserFetch extends AbstractFetchRemote {
|
||||||
[key]: value
|
[key]: value
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
folderMove(sPrevFolderFullName, sNewFolderFullName, bSubscribe) {
|
|
||||||
return this.post('FolderMove', FolderUserStore.foldersRenaming, {
|
|
||||||
folder: sPrevFolderFullName,
|
|
||||||
newFolder: sNewFolderFullName,
|
|
||||||
subscribe: bSubscribe ? 1 : 0
|
|
||||||
});
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new RemoteUserFetch();
|
export default new RemoteUserFetch();
|
||||||
|
|
|
||||||
|
|
@ -117,7 +117,7 @@ export class AbstractSettingsScreen extends AbstractScreen {
|
||||||
rules = {
|
rules = {
|
||||||
subname: /^(.*)$/,
|
subname: /^(.*)$/,
|
||||||
normalize_: (rquest, vals) => {
|
normalize_: (rquest, vals) => {
|
||||||
vals.subname = null == vals.subname ? defaultRoute : pString(vals.subname);
|
vals.subname = pString(vals.subname ?? defaultRoute);
|
||||||
return [vals.subname];
|
return [vals.subname];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,7 @@ export class MailBoxUserScreen extends AbstractScreen {
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
onRoute(folderHash, page, search, messageUid) {
|
onRoute(folderHash, page, search, messageUid) {
|
||||||
|
// Only works when FolderUserStore.folderList() is loaded
|
||||||
const folder = getFolderFromHashMap(folderHash.replace(/~([\d]+)$/, ''));
|
const folder = getFolderFromHashMap(folderHash.replace(/~([\d]+)$/, ''));
|
||||||
if (folder) {
|
if (folder) {
|
||||||
FolderUserStore.currentFolder(folder);
|
FolderUserStore.currentFolder(folder);
|
||||||
|
|
@ -108,7 +109,7 @@ export class MailBoxUserScreen extends AbstractScreen {
|
||||||
*/
|
*/
|
||||||
onBuild() {
|
onBuild() {
|
||||||
doc.addEventListener('click', event =>
|
doc.addEventListener('click', event =>
|
||||||
event.target.closest('#rl-right') && moveAction(false)
|
event.target.closest('#rl-right') && moveAction(0)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,40 @@ import ko from 'ko';
|
||||||
|
|
||||||
import Remote from 'Remote/Admin/Fetch';
|
import Remote from 'Remote/Admin/Fetch';
|
||||||
import { forEachObjectEntry } from 'Common/Utils';
|
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*/ {
|
export class AdminSettingsConfig /*extends AbstractViewSettings*/ {
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.config = ko.observableArray();
|
this.config = ko.observableArray();
|
||||||
|
this.search = ko.observableArray();
|
||||||
this.saved = ko.observable(false).extend({ falseTimeout: 5000 });
|
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() {
|
beforeShow() {
|
||||||
|
|
@ -28,13 +56,19 @@ export class AdminSettingsConfig /*extends AbstractViewSettings*/ {
|
||||||
items: []
|
items: []
|
||||||
};
|
};
|
||||||
forEachObjectEntry(items, (skey, item) => {
|
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 ||
|
'admin_password' === skey ||
|
||||||
section.items.push({
|
section.items.push({
|
||||||
key: `config[${key}][${skey}]`,
|
key: `config[${key}][${skey}]`,
|
||||||
name: skey,
|
name: skey,
|
||||||
value: item[0],
|
value: item[0],
|
||||||
type: getInputType(item[0], skey.includes('password')),
|
type: getInputType(item[0], skey.includes('password')),
|
||||||
comment: item[1]
|
comment: item[1],
|
||||||
|
options: item[2]
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
cfg.push(section);
|
cfg.push(section);
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,8 @@ export class AdminSettingsContacts extends AbstractViewSettings {
|
||||||
this.addSetting('contactsMySQLSSLVerify');
|
this.addSetting('contactsMySQLSSLVerify');
|
||||||
this.addSetting('contactsMySQLSSLCiphers');
|
this.addSetting('contactsMySQLSSLCiphers');
|
||||||
|
|
||||||
|
this.addSetting('contactsSQLiteGlobal');
|
||||||
|
|
||||||
addObservablesTo(this, {
|
addObservablesTo(this, {
|
||||||
testing: false,
|
testing: false,
|
||||||
testContactsSuccess: false,
|
testContactsSuccess: false,
|
||||||
|
|
@ -102,7 +104,8 @@ export class AdminSettingsContacts extends AbstractViewSettings {
|
||||||
PdoPassword: this.contactsPdoPassword(),
|
PdoPassword: this.contactsPdoPassword(),
|
||||||
MySQLSSLCA: this.contactsMySQLSSLCA(),
|
MySQLSSLCA: this.contactsMySQLSSLCA(),
|
||||||
MySQLSSLVerify: this.contactsMySQLSSLVerify(),
|
MySQLSSLVerify: this.contactsMySQLSSLVerify(),
|
||||||
MySQLSSLCiphers: this.contactsMySQLSSLCiphers()
|
MySQLSSLCiphers: this.contactsMySQLSSLCiphers(),
|
||||||
|
SQLiteGlobal: this.contactsSQLiteGlobal()
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,9 @@
|
||||||
import ko from 'ko';
|
import ko from 'ko';
|
||||||
|
|
||||||
import {
|
|
||||||
isArray
|
|
||||||
} from 'Common/Utils';
|
|
||||||
|
|
||||||
import { addObservablesTo, addSubscribablesTo, addComputablesTo } from 'External/ko';
|
import { addObservablesTo, addSubscribablesTo, addComputablesTo } from 'External/ko';
|
||||||
|
|
||||||
import { SaveSettingStatus } from 'Common/Enums';
|
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 { translatorReload, convertLangName } from 'Common/Translator';
|
||||||
|
|
||||||
import { AbstractViewSettings } from 'Knoin/AbstractViews';
|
import { AbstractViewSettings } from 'Knoin/AbstractViews';
|
||||||
|
|
@ -24,11 +20,7 @@ export class AdminSettingsGeneral extends AbstractViewSettings {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.language = LanguageStore.language;
|
this.language = LanguageStore.language;
|
||||||
this.languages = LanguageStore.languages;
|
this.languageAdmin = ko.observable(SettingsAdmin('language'));
|
||||||
|
|
||||||
const aLanguagesAdmin = Settings.app('languagesAdmin');
|
|
||||||
this.languagesAdmin = ko.observableArray(isArray(aLanguagesAdmin) ? aLanguagesAdmin : []);
|
|
||||||
this.languageAdmin = ko.observable(SettingsGet('languageAdmin'));
|
|
||||||
|
|
||||||
this.theme = ThemeStore.theme;
|
this.theme = ThemeStore.theme;
|
||||||
this.themes = ThemeStore.themes;
|
this.themes = ThemeStore.themes;
|
||||||
|
|
@ -107,14 +99,18 @@ export class AdminSettingsGeneral extends AbstractViewSettings {
|
||||||
}
|
}
|
||||||
|
|
||||||
selectLanguage() {
|
selectLanguage() {
|
||||||
showScreenPopup(LanguagesPopupView, [this.language, this.languages(), LanguageStore.userLanguage()]);
|
showScreenPopup(LanguagesPopupView, [
|
||||||
|
this.language,
|
||||||
|
LanguageStore.languages,
|
||||||
|
LanguageStore.userLanguage()
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
selectLanguageAdmin() {
|
selectLanguageAdmin() {
|
||||||
showScreenPopup(LanguagesPopupView, [
|
showScreenPopup(LanguagesPopupView, [
|
||||||
this.languageAdmin,
|
this.languageAdmin,
|
||||||
this.languagesAdmin(),
|
SettingsAdmin('languages'),
|
||||||
SettingsGet('languageUsers')
|
SettingsAdmin('clientLanguage')
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ export class AdminSettingsSecurity extends AbstractViewSettings {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.addSettings(['useLocalProxyForExternalImages']);
|
this.addSettings(['proxyExternalImages', 'autoVerifySignatures']);
|
||||||
|
|
||||||
this.weakPassword = rl.app.weakPassword;
|
this.weakPassword = rl.app.weakPassword;
|
||||||
|
|
||||||
|
|
@ -28,6 +28,7 @@ export class AdminSettingsSecurity extends AbstractViewSettings {
|
||||||
|
|
||||||
viewQRCode: '',
|
viewQRCode: '',
|
||||||
|
|
||||||
|
capaGnuPG: SettingsCapa('GnuPG'),
|
||||||
capaOpenPGP: SettingsCapa('OpenPGP')
|
capaOpenPGP: SettingsCapa('OpenPGP')
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -65,7 +66,8 @@ export class AdminSettingsSecurity extends AbstractViewSettings {
|
||||||
|
|
||||||
adminPasswordNew2: reset,
|
adminPasswordNew2: reset,
|
||||||
|
|
||||||
capaOpenPGP: value => Remote.saveSetting('CapaOpenPGP', value)
|
capaGnuPG: value => Remote.saveSetting('capaGnuPG', value),
|
||||||
|
capaOpenPGP: value => Remote.saveSetting('capaOpenPGP', value)
|
||||||
});
|
});
|
||||||
|
|
||||||
this.adminTOTP(SettingsGet('adminTOTP'));
|
this.adminTOTP(SettingsGet('adminTOTP'));
|
||||||
|
|
@ -75,6 +77,16 @@ export class AdminSettingsSecurity extends AbstractViewSettings {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
generateTOTP() {
|
||||||
|
let CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567',
|
||||||
|
length = 16,
|
||||||
|
secret = '';
|
||||||
|
while (0 < length--) {
|
||||||
|
secret += CHARS[Math.floor(Math.random() * 32)];
|
||||||
|
}
|
||||||
|
this.adminTOTP(secret);
|
||||||
|
}
|
||||||
|
|
||||||
saveAdminUserCommand() {
|
saveAdminUserCommand() {
|
||||||
if (!this.adminLogin().trim()) {
|
if (!this.adminLogin().trim()) {
|
||||||
this.adminLoginError(true);
|
this.adminLoginError(true);
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import ko from 'ko';
|
||||||
|
|
||||||
//import { koComputable } from 'External/ko';
|
//import { koComputable } from 'External/ko';
|
||||||
import { SettingsCapa, SettingsGet } from 'Common/Globals';
|
import { SettingsCapa, SettingsGet } from 'Common/Globals';
|
||||||
|
import { loadAccountsAndIdentities } from 'Common/UtilsUser';
|
||||||
|
|
||||||
import { AccountUserStore } from 'Stores/User/Account';
|
import { AccountUserStore } from 'Stores/User/Account';
|
||||||
import { IdentityUserStore } from 'Stores/User/Identity';
|
import { IdentityUserStore } from 'Stores/User/Identity';
|
||||||
|
|
@ -64,7 +65,7 @@ export class UserSettingsAccounts /*extends AbstractViewSettings*/ {
|
||||||
rl.route.root();
|
rl.route.root();
|
||||||
setTimeout(() => location.reload(), 1);
|
setTimeout(() => location.reload(), 1);
|
||||||
} else {
|
} else {
|
||||||
rl.app.accountsAndIdentities();
|
loadAccountsAndIdentities();
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
emailToDelete: accountToRemove.email
|
emailToDelete: accountToRemove.email
|
||||||
|
|
@ -88,7 +89,7 @@ export class UserSettingsAccounts /*extends AbstractViewSettings*/ {
|
||||||
|
|
||||||
accountsAndIdentitiesAfterMove() {
|
accountsAndIdentitiesAfterMove() {
|
||||||
Remote.request('AccountsAndIdentitiesSortOrder', null, {
|
Remote.request('AccountsAndIdentitiesSortOrder', null, {
|
||||||
Accounts: AccountUserStore.getEmailAddresses().filter(v => v != SettingsGet('mainEmail')),
|
Accounts: AccountUserStore.filter(item => item.isAdditional()).map(item => item.email),
|
||||||
Identities: IdentityUserStore.map(item => (item ? item.id() : ""))
|
Identities: IdentityUserStore.map(item => (item ? item.id() : ""))
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import Remote from 'Remote/User/Fetch';
|
||||||
|
|
||||||
import { showScreenPopup } from 'Knoin/Knoin';
|
import { showScreenPopup } from 'Knoin/Knoin';
|
||||||
|
|
||||||
|
//import { FolderPopupView } from 'View/Popup/Folder';
|
||||||
import { FolderCreatePopupView } from 'View/Popup/FolderCreate';
|
import { FolderCreatePopupView } from 'View/Popup/FolderCreate';
|
||||||
import { FolderSystemPopupView } from 'View/Popup/FolderSystem';
|
import { FolderSystemPopupView } from 'View/Popup/FolderSystem';
|
||||||
|
|
||||||
|
|
@ -108,7 +109,7 @@ export class UserSettingsFolders /*extends AbstractViewSettings*/ {
|
||||||
}
|
}
|
||||||
|
|
||||||
hideError() {
|
hideError() {
|
||||||
this.error('');
|
FolderUserStore.error('');
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleFolderKolabType(folder, event) {
|
toggleFolderKolabType(folder, event) {
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import { SaveSettingStatus } from 'Common/Enums';
|
||||||
import { LayoutSideView, LayoutBottomView } from 'Common/EnumsUser';
|
import { LayoutSideView, LayoutBottomView } from 'Common/EnumsUser';
|
||||||
import { setRefreshFoldersInterval } from 'Common/Folders';
|
import { setRefreshFoldersInterval } from 'Common/Folders';
|
||||||
import { Settings, SettingsGet } from 'Common/Globals';
|
import { Settings, SettingsGet } from 'Common/Globals';
|
||||||
|
import { WYSIWYGS } from 'Common/HtmlEditor';
|
||||||
import { isArray } from 'Common/Utils';
|
import { isArray } from 'Common/Utils';
|
||||||
import { addSubscribablesTo, addComputablesTo } from 'External/ko';
|
import { addSubscribablesTo, addComputablesTo } from 'External/ko';
|
||||||
import { i18n, translateTrigger, translatorReload, convertLangName } from 'Common/Translator';
|
import { i18n, translateTrigger, translatorReload, convertLangName } from 'Common/Translator';
|
||||||
|
|
@ -14,6 +15,7 @@ import { showScreenPopup } from 'Knoin/Knoin';
|
||||||
|
|
||||||
import { AppUserStore } from 'Stores/User/App';
|
import { AppUserStore } from 'Stores/User/App';
|
||||||
import { LanguageStore } from 'Stores/Language';
|
import { LanguageStore } from 'Stores/Language';
|
||||||
|
import { FolderUserStore } from 'Stores/User/Folder';
|
||||||
import { SettingsUserStore } from 'Stores/User/Settings';
|
import { SettingsUserStore } from 'Stores/User/Settings';
|
||||||
import { IdentityUserStore } from 'Stores/User/Identity';
|
import { IdentityUserStore } from 'Stores/User/Identity';
|
||||||
import { NotificationUserStore } from 'Stores/User/Notification';
|
import { NotificationUserStore } from 'Stores/User/Notification';
|
||||||
|
|
@ -40,11 +42,18 @@ export class UserSettingsGeneral extends AbstractViewSettings {
|
||||||
this.isDesktopNotificationAllowed = NotificationUserStore.allowed;
|
this.isDesktopNotificationAllowed = NotificationUserStore.allowed;
|
||||||
|
|
||||||
this.threadsAllowed = AppUserStore.threadsAllowed;
|
this.threadsAllowed = AppUserStore.threadsAllowed;
|
||||||
|
// 'THREAD=REFS', 'THREAD=REFERENCES', 'THREAD=ORDEREDSUBJECT'
|
||||||
|
this.threadAlgorithms = ko.observableArray();
|
||||||
|
FolderUserStore.capabilities.forEach(capa =>
|
||||||
|
capa.startsWith('THREAD=') && this.threadAlgorithms.push(capa.slice(7))
|
||||||
|
);
|
||||||
|
this.threadAlgorithms.sort((a, b) => a.length - b.length);
|
||||||
|
this.threadAlgorithm = SettingsUserStore.threadAlgorithm;
|
||||||
|
|
||||||
['useThreads',
|
['useThreads', 'threadAlgorithm',
|
||||||
// These use addSetting()
|
// These use addSetting()
|
||||||
'layout', 'messageReadDelay', 'messagesPerPage', 'checkMailInterval',
|
'layout', 'messageReadDelay', 'messagesPerPage', 'checkMailInterval',
|
||||||
'editorDefaultType', 'msgDefaultAction', 'maxBlockquotesLevel',
|
'editorDefaultType', 'editorWysiwyg', 'msgDefaultAction', 'maxBlockquotesLevel',
|
||||||
// These are in addSettings()
|
// These are in addSettings()
|
||||||
'requestReadReceipt', 'requestDsn', 'requireTLS', 'pgpSign', 'pgpEncrypt',
|
'requestReadReceipt', 'requestDsn', 'requireTLS', 'pgpSign', 'pgpEncrypt',
|
||||||
'viewHTML', 'viewImages', 'viewImagesWhitelist', 'removeColors', 'allowStyles', 'allowDraftAutosave',
|
'viewHTML', 'viewImages', 'viewImagesWhitelist', 'removeColors', 'allowStyles', 'allowDraftAutosave',
|
||||||
|
|
@ -59,6 +68,8 @@ export class UserSettingsGeneral extends AbstractViewSettings {
|
||||||
|
|
||||||
this.identities = IdentityUserStore;
|
this.identities = IdentityUserStore;
|
||||||
|
|
||||||
|
this.wysiwygs = WYSIWYGS;
|
||||||
|
|
||||||
addComputablesTo(this, {
|
addComputablesTo(this, {
|
||||||
languageFullName: () => convertLangName(this.language()),
|
languageFullName: () => convertLangName(this.language()),
|
||||||
|
|
||||||
|
|
@ -80,6 +91,8 @@ export class UserSettingsGeneral extends AbstractViewSettings {
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
|
|
||||||
|
hasWysiwygs: () => 1 < WYSIWYGS().length,
|
||||||
|
|
||||||
msgDefaultActions: () => {
|
msgDefaultActions: () => {
|
||||||
translateTrigger();
|
translateTrigger();
|
||||||
return [
|
return [
|
||||||
|
|
@ -99,6 +112,7 @@ export class UserSettingsGeneral extends AbstractViewSettings {
|
||||||
});
|
});
|
||||||
|
|
||||||
this.addSetting('EditorDefaultType');
|
this.addSetting('EditorDefaultType');
|
||||||
|
this.addSetting('editorWysiwyg');
|
||||||
this.addSetting('MsgDefaultAction');
|
this.addSetting('MsgDefaultAction');
|
||||||
this.addSetting('MessageReadDelay');
|
this.addSetting('MessageReadDelay');
|
||||||
this.addSetting('MessagesPerPage');
|
this.addSetting('MessagesPerPage');
|
||||||
|
|
@ -140,6 +154,11 @@ export class UserSettingsGeneral extends AbstractViewSettings {
|
||||||
Remote.saveSetting('UseThreads', value);
|
Remote.saveSetting('UseThreads', value);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
threadAlgorithm: value => {
|
||||||
|
MessagelistUserStore([]);
|
||||||
|
Remote.saveSetting('threadAlgorithm', value);
|
||||||
|
},
|
||||||
|
|
||||||
checkMailInterval: () => {
|
checkMailInterval: () => {
|
||||||
setRefreshFoldersInterval(SettingsUserStore.checkMailInterval());
|
setRefreshFoldersInterval(SettingsUserStore.checkMailInterval());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,8 @@ import { showScreenPopup } from 'Knoin/Knoin';
|
||||||
import { OpenPgpImportPopupView } from 'View/Popup/OpenPgpImport';
|
import { OpenPgpImportPopupView } from 'View/Popup/OpenPgpImport';
|
||||||
import { OpenPgpGeneratePopupView } from 'View/Popup/OpenPgpGenerate';
|
import { OpenPgpGeneratePopupView } from 'View/Popup/OpenPgpGenerate';
|
||||||
|
|
||||||
//import Remote from 'Remote/User/Fetch';
|
import { SMimeUserStore } from 'Stores/User/SMime';
|
||||||
|
import { SMimeImportPopupView } from 'View/Popup/SMimeImport';
|
||||||
|
|
||||||
export class UserSettingsSecurity extends AbstractViewSettings {
|
export class UserSettingsSecurity extends AbstractViewSettings {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|
@ -43,6 +44,8 @@ export class UserSettingsSecurity extends AbstractViewSettings {
|
||||||
this.openpgpkeysPublic = OpenPGPUserStore.publicKeys;
|
this.openpgpkeysPublic = OpenPGPUserStore.publicKeys;
|
||||||
this.openpgpkeysPrivate = OpenPGPUserStore.privateKeys;
|
this.openpgpkeysPrivate = OpenPGPUserStore.privateKeys;
|
||||||
|
|
||||||
|
this.smimeCertificates = SMimeUserStore;
|
||||||
|
|
||||||
this.canOpenPGP = SettingsCapa('OpenPGP');
|
this.canOpenPGP = SettingsCapa('OpenPGP');
|
||||||
this.canGnuPG = GnuPGUserStore.isSupported();
|
this.canGnuPG = GnuPGUserStore.isSupported();
|
||||||
this.canMailvelope = !!window.mailvelope;
|
this.canMailvelope = !!window.mailvelope;
|
||||||
|
|
@ -56,6 +59,14 @@ export class UserSettingsSecurity extends AbstractViewSettings {
|
||||||
showScreenPopup(OpenPgpGeneratePopupView);
|
showScreenPopup(OpenPgpGeneratePopupView);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
importToOpenPGP() {
|
||||||
|
OpenPGPUserStore.loadBackupKeys();
|
||||||
|
}
|
||||||
|
|
||||||
|
importToSMime() {
|
||||||
|
showScreenPopup(SMimeImportPopupView);
|
||||||
|
}
|
||||||
|
|
||||||
onBuild() {
|
onBuild() {
|
||||||
/**
|
/**
|
||||||
* Create an iframe to display the Mailvelope keyring settings.
|
* Create an iframe to display the Mailvelope keyring settings.
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ export class AbstractModel {
|
||||||
constructor() {
|
constructor() {
|
||||||
/*
|
/*
|
||||||
if (new.target === AbstractModel) {
|
if (new.target === AbstractModel) {
|
||||||
throw new Error("Can't instantiate AbstractModel!");
|
throw Error("Can't instantiate AbstractModel!");
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
Object.defineProperty(this, 'disposables', {value: []});
|
Object.defineProperty(this, 'disposables', {value: []});
|
||||||
|
|
|
||||||
|
|
@ -30,8 +30,6 @@ const
|
||||||
sDeepPrefix = '\u00A0\u00A0\u00A0',
|
sDeepPrefix = '\u00A0\u00A0\u00A0',
|
||||||
showUnsubscribed = true/*!SettingsUserStore.hideUnsubscribed()*/,
|
showUnsubscribed = true/*!SettingsUserStore.hideUnsubscribed()*/,
|
||||||
|
|
||||||
disabled = rl.settings.get('sieveAllowFileintoInbox') ? '' : 'INBOX',
|
|
||||||
|
|
||||||
foldersWalk = folders => {
|
foldersWalk = folders => {
|
||||||
folders.forEach(oItem => {
|
folders.forEach(oItem => {
|
||||||
if (showUnsubscribed || oItem.hasSubscriptions() || !oItem.exists) {
|
if (showUnsubscribed || oItem.hasSubscriptions() || !oItem.exists) {
|
||||||
|
|
@ -39,7 +37,7 @@ const
|
||||||
id: oItem.fullName,
|
id: oItem.fullName,
|
||||||
name: sDeepPrefix.repeat(oItem.deep) + oItem.detailedName(),
|
name: sDeepPrefix.repeat(oItem.deep) + oItem.detailedName(),
|
||||||
system: false,
|
system: false,
|
||||||
disabled: !oItem.selectable() || disabled == oItem.fullName
|
disabled: !oItem.selectable()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ try {
|
||||||
data = data ? decodeURIComponent(data[2]) : null;
|
data = data ? decodeURIComponent(data[2]) : null;
|
||||||
data = data ? JSON.parse(data) : {};
|
data = data ? JSON.parse(data) : {};
|
||||||
win[sName] = {
|
win[sName] = {
|
||||||
getItem: key => data[key] == null ? null : data[key],
|
getItem: key => data[key] ?? null,
|
||||||
setItem: (key, value) => {
|
setItem: (key, value) => {
|
||||||
data[key] = ''+value; // forces the value to a string
|
data[key] = ''+value; // forces the value to a string
|
||||||
document.cookie = sName+'='+encodeURIComponent(JSON.stringify(data))
|
document.cookie = sName+'='+encodeURIComponent(JSON.stringify(data))
|
||||||
|
|
|
||||||
|
|
@ -1 +1,19 @@
|
||||||
export const Passphrases = new Map();
|
import { AskPopupView } from 'View/Popup/Ask';
|
||||||
|
|
||||||
|
export const Passphrases = new WeakMap();
|
||||||
|
|
||||||
|
Passphrases.ask = async (key, sAskDesc, btnText) =>
|
||||||
|
Passphrases.has(key)
|
||||||
|
? {password:Passphrases.handle(key)/*, remember:false*/}
|
||||||
|
: await AskPopupView.password(sAskDesc, btnText, 5);
|
||||||
|
|
||||||
|
const timeouts = {};
|
||||||
|
// get/set accessor to control deletion after 15 minutes of inactivity
|
||||||
|
Passphrases.handle = (key, pass) => {
|
||||||
|
if (!timeouts[key]) {
|
||||||
|
timeouts[key] = (()=>Passphrases.delete(key)).debounce(900000);
|
||||||
|
}
|
||||||
|
pass && Passphrases.set(key, pass);
|
||||||
|
timeouts[key]();
|
||||||
|
return Passphrases.get(key);
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ DomainAdminStore.fetch = () => {
|
||||||
if (!iError) {
|
if (!iError) {
|
||||||
DomainAdminStore(
|
DomainAdminStore(
|
||||||
data.Result.map(item => {
|
data.Result.map(item => {
|
||||||
|
item.name = IDN.toUnicode(item.name);
|
||||||
item.disabled = ko.observable(item.disabled);
|
item.disabled = ko.observable(item.disabled);
|
||||||
item.askDelete = ko.observable(false);
|
item.askDelete = ko.observable(false);
|
||||||
return item;
|
return item;
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ export const LanguageStore = {
|
||||||
const aLanguages = Settings.app('languages');
|
const aLanguages = Settings.app('languages');
|
||||||
this.languages(isArray(aLanguages) ? aLanguages : []);
|
this.languages(isArray(aLanguages) ? aLanguages : []);
|
||||||
this.language(SettingsGet('language'));
|
this.language(SettingsGet('language'));
|
||||||
this.userLanguage(SettingsGet('userLanguage'));
|
this.userLanguage(SettingsGet('clientLanguage'));
|
||||||
this.hourCycle(SettingsGet('hourCycle'));
|
this.hourCycle(SettingsGet('hourCycle'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,6 @@ import { addObservablesTo, koArrayWithDestroy } from 'External/ko';
|
||||||
|
|
||||||
export const AccountUserStore = koArrayWithDestroy();
|
export const AccountUserStore = koArrayWithDestroy();
|
||||||
|
|
||||||
AccountUserStore.getEmailAddresses = () => AccountUserStore.map(item => item.email);
|
|
||||||
|
|
||||||
addObservablesTo(AccountUserStore, {
|
addObservablesTo(AccountUserStore, {
|
||||||
email: '',
|
email: '',
|
||||||
loading: false
|
loading: false
|
||||||
|
|
|
||||||
|
|
@ -9,25 +9,17 @@ import Remote from 'Remote/User/Fetch';
|
||||||
|
|
||||||
import { showScreenPopup } from 'Knoin/Knoin';
|
import { showScreenPopup } from 'Knoin/Knoin';
|
||||||
import { OpenPgpKeyPopupView } from 'View/Popup/OpenPgpKey';
|
import { OpenPgpKeyPopupView } from 'View/Popup/OpenPgpKey';
|
||||||
import { AskPopupView } from 'View/Popup/Ask';
|
|
||||||
|
|
||||||
import { Passphrases } from 'Storage/Passphrases';
|
import { Passphrases } from 'Storage/Passphrases';
|
||||||
|
|
||||||
const
|
import { baseCollator } from 'Common/Translator';
|
||||||
askPassphrase = async (privateKey, btnTxt = 'LABEL_SIGN') => {
|
|
||||||
const key = privateKey.id,
|
|
||||||
pass = Passphrases.has(key)
|
|
||||||
? {password:Passphrases.get(key), remember:false}
|
|
||||||
: await AskPopupView.password('GnuPG key<br>' + key + ' ' + privateKey.emails[0], 'OPENPGP/'+btnTxt);
|
|
||||||
pass && pass.remember && Passphrases.set(key, pass.password);
|
|
||||||
return pass.password;
|
|
||||||
},
|
|
||||||
|
|
||||||
|
const
|
||||||
findGnuPGKey = (keys, query/*, sign*/) =>
|
findGnuPGKey = (keys, query/*, sign*/) =>
|
||||||
keys.find(key =>
|
keys.find(key =>
|
||||||
// key[sign ? 'can_sign' : 'can_decrypt']
|
// key[sign ? 'can_sign' : 'can_decrypt']
|
||||||
(key.can_sign || key.can_decrypt)
|
(key.can_sign || key.can_decrypt)
|
||||||
&& (key.emails.includes(query) || key.subkeys.find(key => query == key.keyid || query == key.fingerprint))
|
&& (key.for(query) || key.subkeys.find(key => query == key.keyid || query == key.fingerprint))
|
||||||
);
|
);
|
||||||
|
|
||||||
export const GnuPGUserStore = new class {
|
export const GnuPGUserStore = new class {
|
||||||
|
|
@ -45,7 +37,8 @@ export const GnuPGUserStore = new class {
|
||||||
this.keyring = null;
|
this.keyring = null;
|
||||||
this.publicKeys([]);
|
this.publicKeys([]);
|
||||||
this.privateKeys([]);
|
this.privateKeys([]);
|
||||||
Remote.request('GnupgGetKeys',
|
SettingsCapa('GnuPG')
|
||||||
|
&& Remote.request('GnupgGetKeys',
|
||||||
(iError, oData) => {
|
(iError, oData) => {
|
||||||
if (oData?.Result) {
|
if (oData?.Result) {
|
||||||
this.keyring = oData.Result;
|
this.keyring = oData.Result;
|
||||||
|
|
@ -55,6 +48,7 @@ export const GnuPGUserStore = new class {
|
||||||
key.fingerprint = key.subkeys[0].fingerprint;
|
key.fingerprint = key.subkeys[0].fingerprint;
|
||||||
key.uids.forEach(uid => uid.email && aEmails.push(uid.email));
|
key.uids.forEach(uid => uid.email && aEmails.push(uid.email));
|
||||||
key.emails = aEmails;
|
key.emails = aEmails;
|
||||||
|
key.for = email => aEmails.includes(IDN.toASCII(email));
|
||||||
key.askDelete = ko.observable(false);
|
key.askDelete = ko.observable(false);
|
||||||
key.openForDeletion = ko.observable(null).askDeleteHelper();
|
key.openForDeletion = ko.observable(null).askDeleteHelper();
|
||||||
key.remove = () => {
|
key.remove = () => {
|
||||||
|
|
@ -77,33 +71,49 @@ export const GnuPGUserStore = new class {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
key.view = () => {
|
if (isPrivate) {
|
||||||
const fetch = pass => Remote.request('GnupgExportKey',
|
key.password = async btnTxt => {
|
||||||
(iError, oData) => {
|
const pass = await Passphrases.ask(key,
|
||||||
if (oData?.Result) {
|
'GnuPG key<br>' + key.id + ' ' + key.emails[0],
|
||||||
key.armor = oData.Result;
|
btnTxt
|
||||||
showScreenPopup(OpenPgpKeyPopupView, [key]);
|
|
||||||
} else {
|
|
||||||
Passphrases.delete(key.id);
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
keyId: key.id,
|
|
||||||
isPrivate: isPrivate,
|
|
||||||
passphrase: pass
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
if (isPrivate) {
|
pass && pass.remember && Passphrases.handle(key, pass.password);
|
||||||
askPassphrase(key, 'POPUP_VIEW_TITLE').then(passphrase => {
|
return pass?.password;
|
||||||
(null !== passphrase) && fetch(passphrase);
|
};
|
||||||
});
|
}
|
||||||
|
key.fetch = async callback => {
|
||||||
|
if (key.armor) {
|
||||||
|
callback && callback();
|
||||||
} else {
|
} else {
|
||||||
fetch('');
|
let pass = isPrivate ? await key.password('OPENPGP/POPUP_VIEW_TITLE') : '';
|
||||||
|
if (null != pass) try {
|
||||||
|
const result = await Remote.post('GnupgExportKey', null, {
|
||||||
|
keyId: key.id,
|
||||||
|
isPrivate: isPrivate,
|
||||||
|
passphrase: pass
|
||||||
|
});
|
||||||
|
if (result?.Result) {
|
||||||
|
key.armor = result.Result;
|
||||||
|
callback && callback();
|
||||||
|
} else {
|
||||||
|
Passphrases.delete(key);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
Passphrases.delete(key);
|
||||||
|
alert(e.message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
return key.armor;
|
||||||
};
|
};
|
||||||
|
key.view = () => key.fetch(() => showScreenPopup(OpenPgpKeyPopupView, [key]));
|
||||||
return key;
|
return key;
|
||||||
};
|
},
|
||||||
this.publicKeys(oData.Result.public.map(key => initKey(key, 0)));
|
collator = baseCollator(),
|
||||||
this.privateKeys(oData.Result.private.map(key => initKey(key, 1)));
|
sort = keys => keys.sort(
|
||||||
|
(a, b) => collator.compare(a.emails[0], b.emails[0]) || collator.compare(a.id, b.id)
|
||||||
|
);
|
||||||
|
this.publicKeys(sort(oData.Result.public.map(key => initKey(key, 0))));
|
||||||
|
this.privateKeys(sort(oData.Result.private.map(key => initKey(key, 1))));
|
||||||
console.log('gnupg ready');
|
console.log('gnupg ready');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -117,19 +127,6 @@ export const GnuPGUserStore = new class {
|
||||||
return SettingsCapa('GnuPG');
|
return SettingsCapa('GnuPG');
|
||||||
}
|
}
|
||||||
|
|
||||||
importKey(key, callback) {
|
|
||||||
Remote.request('GnupgImportKey',
|
|
||||||
(iError, oData) => {
|
|
||||||
if (oData?.Result/* && (oData.Result.imported || oData.Result.secretimported)*/) {
|
|
||||||
this.loadKeyrings();
|
|
||||||
}
|
|
||||||
callback?.(iError, oData);
|
|
||||||
}, {
|
|
||||||
key: key
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
keyPair.privateKey
|
keyPair.privateKey
|
||||||
keyPair.publicKey
|
keyPair.publicKey
|
||||||
|
|
@ -155,7 +152,7 @@ export const GnuPGUserStore = new class {
|
||||||
const count = recipients.length,
|
const count = recipients.length,
|
||||||
length = count ? recipients.filter(email =>
|
length = count ? recipients.filter(email =>
|
||||||
// (key.can_verify || key.can_encrypt) &&
|
// (key.can_verify || key.can_encrypt) &&
|
||||||
this.publicKeys.find(key => key.emails.includes(email))
|
this.publicKeys.find(key => key.for(email))
|
||||||
).length : 0;
|
).length : 0;
|
||||||
return length && length === count;
|
return length && length === count;
|
||||||
}
|
}
|
||||||
|
|
@ -163,13 +160,13 @@ export const GnuPGUserStore = new class {
|
||||||
getPublicKeyFingerprints(recipients) {
|
getPublicKeyFingerprints(recipients) {
|
||||||
const fingerprints = [];
|
const fingerprints = [];
|
||||||
recipients.forEach(email => {
|
recipients.forEach(email => {
|
||||||
fingerprints.push(this.publicKeys.find(key => key.emails.includes(email)).fingerprint);
|
fingerprints.push(this.publicKeys.find(key => key.for(email)).fingerprint);
|
||||||
});
|
});
|
||||||
return fingerprints;
|
return fingerprints;
|
||||||
}
|
}
|
||||||
|
|
||||||
getPrivateKeyFor(query, sign) {
|
getPrivateKeyFor(query/*, sign*/) {
|
||||||
return findGnuPGKey(this.privateKeys, query, sign);
|
return findGnuPGKey(this.privateKeys, query/*, sign*/);
|
||||||
}
|
}
|
||||||
|
|
||||||
async decrypt(message) {
|
async decrypt(message) {
|
||||||
|
|
@ -191,22 +188,27 @@ export const GnuPGUserStore = new class {
|
||||||
uid: message.uid,
|
uid: message.uid,
|
||||||
partId: pgpInfo.partId,
|
partId: pgpInfo.partId,
|
||||||
keyId: key.id,
|
keyId: key.id,
|
||||||
passphrase: await askPassphrase(key, 'BUTTON_DECRYPT'),
|
passphrase: await key.password('CRYPTO/DECRYPT'),
|
||||||
data: '' // message.plain() optional
|
data: '' // message.plain() optional
|
||||||
}
|
}
|
||||||
if (null !== params.passphrase) {
|
if (null != params.passphrase) {
|
||||||
const result = await Remote.post('GnupgDecrypt', null, params);
|
try {
|
||||||
if (result?.Result && false !== result.Result.data) {
|
const response = await Remote.post('GnupgDecrypt', null, params);
|
||||||
return result.Result;
|
if (response?.Result?.data) {
|
||||||
|
return response.Result;
|
||||||
|
}
|
||||||
|
throw response;
|
||||||
|
} catch (e) {
|
||||||
|
Passphrases.delete(key);
|
||||||
|
throw e;
|
||||||
}
|
}
|
||||||
Passphrases.delete(key.id);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async verify(message) {
|
async verify(message) {
|
||||||
let data = message.pgpSigned(); // { bodyPartId: "1", sigPartId: "2", micAlg: "pgp-sha256" }
|
let data = message.pgpSigned(); // { partId: "1", sigPartId: "2", micAlg: "pgp-sha256" }
|
||||||
if (data) {
|
if (data) {
|
||||||
data = { ...data }; // clone
|
data = { ...data }; // clone
|
||||||
// const sender = message.from[0].email;
|
// const sender = message.from[0].email;
|
||||||
|
|
@ -217,18 +219,19 @@ export const GnuPGUserStore = new class {
|
||||||
data.bodyPart = data.bodyPart.raw;
|
data.bodyPart = data.bodyPart.raw;
|
||||||
data.sigPart = data.sigPart.body;
|
data.sigPart = data.sigPart.body;
|
||||||
}
|
}
|
||||||
let response = await Remote.post('MessagePgpVerify', null, data);
|
let response = await Remote.post('PgpVerifyMessage', null, data);
|
||||||
if (response?.Result) {
|
if (response?.Result) {
|
||||||
return {
|
return {
|
||||||
fingerprint: response.Result.fingerprint,
|
fingerprint: response.Result.fingerprint,
|
||||||
success: 0 == response.Result.status // GOODSIG
|
success: 0 == response.Result.status, // GOODSIG
|
||||||
|
error: response.Result.message
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async sign(privateKey) {
|
async sign(privateKey) {
|
||||||
return await askPassphrase(privateKey);
|
return await privateKey.password('CRYPTO/SIGN');
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
124
dev/Stores/User/Mailvelope.js
Normal file
124
dev/Stores/User/Mailvelope.js
Normal file
|
|
@ -0,0 +1,124 @@
|
||||||
|
import { SettingsGet } from 'Common/Globals';
|
||||||
|
|
||||||
|
// https://mailvelope.github.io/mailvelope/Keyring.html
|
||||||
|
let mailvelopeKeyring = null;
|
||||||
|
|
||||||
|
export const MailvelopeUserStore = {
|
||||||
|
keyring: null,
|
||||||
|
|
||||||
|
loadKeyring(identifier) {
|
||||||
|
identifier = identifier || SettingsGet('Email');
|
||||||
|
if (window.mailvelope) {
|
||||||
|
const fn = keyring => {
|
||||||
|
mailvelopeKeyring = keyring;
|
||||||
|
console.log('mailvelope ready');
|
||||||
|
};
|
||||||
|
mailvelope.getKeyring().then(fn, err => {
|
||||||
|
if (identifier) {
|
||||||
|
// attempt to create a new keyring for this app/user
|
||||||
|
mailvelope.createKeyring(identifier).then(fn, err => console.error(err));
|
||||||
|
} else {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
addEventListener('mailvelope-disconnect', event => {
|
||||||
|
alert('Mailvelope is updated to version ' + event.detail.version + '. Reload page');
|
||||||
|
}, false);
|
||||||
|
} else {
|
||||||
|
addEventListener('mailvelope', () => this.loadKeyring(identifier));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async hasPublicKeyForEmails(recipients) {
|
||||||
|
const
|
||||||
|
mailvelope = mailvelopeKeyring && await mailvelopeKeyring.validKeyForAddress(recipients)
|
||||||
|
/*.then(LookupResult => Object.entries(LookupResult))*/,
|
||||||
|
entries = mailvelope && Object.entries(mailvelope);
|
||||||
|
return entries && entries.filter(value => value[1]).length === recipients.length;
|
||||||
|
},
|
||||||
|
|
||||||
|
async getPrivateKeyFor(email/*, sign*/) {
|
||||||
|
if (mailvelopeKeyring && await mailvelopeKeyring.hasPrivateKey({email:email})) {
|
||||||
|
return ['mailvelope', email];
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
|
||||||
|
async decrypt(message) {
|
||||||
|
// Try Mailvelope (does not support inline images)
|
||||||
|
if (mailvelopeKeyring) {
|
||||||
|
const sender = message.from[0].email,
|
||||||
|
armoredText = message.plain();
|
||||||
|
try {
|
||||||
|
let emails = [...message.from,...message.to,...message.cc].validUnique(),
|
||||||
|
i = emails.length;
|
||||||
|
while (i--) {
|
||||||
|
if (await this.getPrivateKeyFor(emails[i].email)) {
|
||||||
|
/**
|
||||||
|
* https://mailvelope.github.io/mailvelope/Mailvelope.html#createEncryptedFormContainer
|
||||||
|
* Creates an iframe to display an encrypted form
|
||||||
|
*/
|
||||||
|
// mailvelope.createEncryptedFormContainer('#mailvelope-form');
|
||||||
|
/**
|
||||||
|
* https://mailvelope.github.io/mailvelope/Mailvelope.html#createDisplayContainer
|
||||||
|
* Creates an iframe to display the decrypted content of the encrypted mail.
|
||||||
|
*/
|
||||||
|
const body = message.body;
|
||||||
|
body.textContent = '';
|
||||||
|
let result = await mailvelope.createDisplayContainer(
|
||||||
|
'#'+body.id,
|
||||||
|
armoredText,
|
||||||
|
mailvelopeKeyring,
|
||||||
|
{
|
||||||
|
senderAddress: sender
|
||||||
|
// emails[i].email
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (result) {
|
||||||
|
if (result.error?.message) {
|
||||||
|
if ('PWD_DIALOG_CANCEL' !== result.error.code) {
|
||||||
|
alert(result.error.code + ': ' + result.error.message);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
body.classList.add('mailvelope');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Returns headers that should be added to an outgoing email.
|
||||||
|
* So far this is only the autocrypt header.
|
||||||
|
*/
|
||||||
|
/*
|
||||||
|
mailvelopeKeyring.additionalHeadersForOutgoingEmail(from: 'abc@web.de')
|
||||||
|
.then(function(additional) {
|
||||||
|
console.log('additionalHeadersForOutgoingEmail', additional);
|
||||||
|
// logs: {autocrypt: "addr=abc@web.de; prefer-encrypt=mutual; keydata=..."}
|
||||||
|
});
|
||||||
|
|
||||||
|
mailvelopeKeyring.addSyncHandler(syncHandlerObj)
|
||||||
|
mailvelopeKeyring.createKeyBackupContainer(selector, options)
|
||||||
|
mailvelopeKeyring.createKeyGenContainer(selector, {
|
||||||
|
// userIds: [],
|
||||||
|
keySize: 4096
|
||||||
|
})
|
||||||
|
|
||||||
|
mailvelopeKeyring.exportOwnPublicKey(emailAddr).then(<AsciiArmored, Error>)
|
||||||
|
mailvelopeKeyring.importPublicKey(armored)
|
||||||
|
|
||||||
|
// https://mailvelope.github.io/mailvelope/global.html#SyncHandlerObject
|
||||||
|
mailvelopeKeyring.addSyncHandler({
|
||||||
|
uploadSync
|
||||||
|
downloadSync
|
||||||
|
backup
|
||||||
|
restore
|
||||||
|
});
|
||||||
|
*/
|
||||||
|
};
|
||||||
|
|
@ -32,6 +32,8 @@ import { SettingsGet } from 'Common/Globals';
|
||||||
import { SUB_QUERY_PREFIX } from 'Common/Links';
|
import { SUB_QUERY_PREFIX } from 'Common/Links';
|
||||||
import { AppUserStore } from 'Stores/User/App';
|
import { AppUserStore } from 'Stores/User/App';
|
||||||
|
|
||||||
|
import { baseCollator } from 'Common/Translator';
|
||||||
|
|
||||||
const
|
const
|
||||||
isChecked = item => item.checked(),
|
isChecked = item => item.checked(),
|
||||||
replaceHash = hash => {
|
replaceHash = hash => {
|
||||||
|
|
@ -254,17 +256,14 @@ MessagelistUserStore.reload = (bDropPagePosition = false, bDropCurrentFolderCach
|
||||||
|
|
||||||
let flags = folderInfo.permanentFlags || [];
|
let flags = folderInfo.permanentFlags || [];
|
||||||
if (flags.includes('\\*')) {
|
if (flags.includes('\\*')) {
|
||||||
|
/** Add Thunderbird labels */
|
||||||
let i = 6;
|
let i = 6;
|
||||||
while (--i) {
|
while (--i) {
|
||||||
flags.includes('$label'+i) || flags.push('$label'+i);
|
flags.includes('$label'+i) || flags.push('$label'+i);
|
||||||
}
|
}
|
||||||
|
/** TODO: add others by default? */
|
||||||
}
|
}
|
||||||
flags.sort((a, b) => {
|
folder.permanentFlags(flags.sort(baseCollator().compare));
|
||||||
a = a.toUpperCase();
|
|
||||||
b = b.toUpperCase();
|
|
||||||
return (a < b) ? -1 : ((a > b) ? 1 : 0);
|
|
||||||
});
|
|
||||||
folder.permanentFlags(flags);
|
|
||||||
|
|
||||||
MessagelistUserStore.notifyNewMessages(folder.fullName, collection.newMessages);
|
MessagelistUserStore.notifyNewMessages(folder.fullName, collection.newMessages);
|
||||||
}
|
}
|
||||||
|
|
@ -314,6 +313,7 @@ MessagelistUserStore.reload = (bDropPagePosition = false, bDropCurrentFolderCach
|
||||||
|
|
||||||
if (AppUserStore.threadsAllowed() && SettingsUserStore.useThreads()) {
|
if (AppUserStore.threadsAllowed() && SettingsUserStore.useThreads()) {
|
||||||
params.useThreads = 1;
|
params.useThreads = 1;
|
||||||
|
params.threadAlgorithm = SettingsUserStore.threadAlgorithm();
|
||||||
params.threadUid = MessagelistUserStore.threadUid();
|
params.threadUid = MessagelistUserStore.threadUid();
|
||||||
} else {
|
} else {
|
||||||
params.threadUid = 0;
|
params.threadUid = 0;
|
||||||
|
|
|
||||||
|
|
@ -10,38 +10,48 @@ import Remote from 'Remote/User/Fetch';
|
||||||
|
|
||||||
import { showScreenPopup } from 'Knoin/Knoin';
|
import { showScreenPopup } from 'Knoin/Knoin';
|
||||||
import { OpenPgpKeyPopupView } from 'View/Popup/OpenPgpKey';
|
import { OpenPgpKeyPopupView } from 'View/Popup/OpenPgpKey';
|
||||||
import { AskPopupView } from 'View/Popup/Ask';
|
|
||||||
|
|
||||||
import { Passphrases } from 'Storage/Passphrases';
|
import { Passphrases } from 'Storage/Passphrases';
|
||||||
|
|
||||||
|
import { baseCollator } from 'Common/Translator';
|
||||||
|
|
||||||
const
|
const
|
||||||
|
loaded = () => !!window.openpgp,
|
||||||
|
|
||||||
findOpenPGPKey = (keys, query/*, sign*/) =>
|
findOpenPGPKey = (keys, query/*, sign*/) =>
|
||||||
keys.find(key =>
|
keys.find(key =>
|
||||||
key.emails.includes(query) || query == key.id || query == key.fingerprint
|
key.for(query) || query == key.id || query == key.fingerprint
|
||||||
),
|
),
|
||||||
|
|
||||||
decryptKey = async (privateKey, btnTxt = 'LABEL_SIGN') => {
|
decryptKey = async (privateKey, btnTxt = 'SIGN') => {
|
||||||
if (privateKey.key.isDecrypted()) {
|
if (privateKey.key.isDecrypted()) {
|
||||||
return privateKey.key;
|
return privateKey.key;
|
||||||
}
|
}
|
||||||
const key = privateKey.id,
|
const key = privateKey.id,
|
||||||
pass = Passphrases.has(key)
|
pass = await Passphrases.ask(privateKey,
|
||||||
? {password:Passphrases.get(key), remember:false}
|
'OpenPGP.js key<br>' + key + ' ' + privateKey.emails[0],
|
||||||
: await AskPopupView.password(
|
'CRYPTO/'+btnTxt
|
||||||
'OpenPGP.js key<br>' + key + ' ' + privateKey.emails[0],
|
);
|
||||||
'OPENPGP/'+btnTxt
|
|
||||||
);
|
|
||||||
if (pass) {
|
if (pass) {
|
||||||
const passphrase = pass.password,
|
const passphrase = pass.password,
|
||||||
result = await openpgp.decryptKey({
|
result = await openpgp.decryptKey({
|
||||||
privateKey: privateKey.key,
|
privateKey: privateKey.key,
|
||||||
passphrase
|
passphrase
|
||||||
});
|
});
|
||||||
result && pass.remember && Passphrases.set(key, passphrase);
|
result && pass.remember && Passphrases.handle(privateKey, passphrase);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
collator = baseCollator(),
|
||||||
|
sort = keys => keys.sort(
|
||||||
|
// (a, b) => collator.compare(a.emails[0], b.emails[0]) || collator.compare(a.fingerprint, b.fingerprint)
|
||||||
|
(a, b) => collator.compare(a.emails[0], b.emails[0]) || collator.compare(a.id, b.id)
|
||||||
|
),
|
||||||
|
dedup = keys => sort((keys || [])
|
||||||
|
.filter((v, i, a) => a.findIndex(entry => entry.fingerprint == v.fingerprint) === i)
|
||||||
|
),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* OpenPGP.js v5 removed the localStorage (keyring)
|
* OpenPGP.js v5 removed the localStorage (keyring)
|
||||||
* This should be compatible with the old OpenPGP.js v2
|
* This should be compatible with the old OpenPGP.js v2
|
||||||
|
|
@ -53,9 +63,11 @@ const
|
||||||
let keys = [], key,
|
let keys = [], key,
|
||||||
armoredKeys = JSON.parse(storage.getItem(itemname)),
|
armoredKeys = JSON.parse(storage.getItem(itemname)),
|
||||||
i = arrayLength(armoredKeys);
|
i = arrayLength(armoredKeys);
|
||||||
while (i--) {
|
while (i--) try {
|
||||||
key = await openpgp.readKey({armoredKey:armoredKeys[i]});
|
key = await openpgp.readKey({armoredKey:armoredKeys[i]});
|
||||||
key.err || keys.push(new OpenPgpKeyModel(armoredKeys[i], key));
|
key.err || keys.push(new OpenPgpKeyModel(armoredKeys[i], key));
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
}
|
}
|
||||||
return keys;
|
return keys;
|
||||||
},
|
},
|
||||||
|
|
@ -71,15 +83,11 @@ const
|
||||||
class OpenPgpKeyModel {
|
class OpenPgpKeyModel {
|
||||||
constructor(armor, key) {
|
constructor(armor, key) {
|
||||||
this.key = key;
|
this.key = key;
|
||||||
const aEmails = [];
|
|
||||||
if (key.users) {
|
|
||||||
key.users.forEach(user => user.userID.email && aEmails.push(user.userID.email));
|
|
||||||
}
|
|
||||||
this.id = key.getKeyID().toHex().toUpperCase();
|
this.id = key.getKeyID().toHex().toUpperCase();
|
||||||
this.fingerprint = key.getFingerprint();
|
this.fingerprint = key.getFingerprint();
|
||||||
this.can_encrypt = !!key.getEncryptionKey();
|
this.can_encrypt = !!key.getEncryptionKey();
|
||||||
this.can_sign = !!key.getSigningKey();
|
this.can_sign = !!key.getSigningKey();
|
||||||
this.emails = aEmails;
|
this.emails = key.users.map(user => IDN.toASCII(user.userID.email)).filter(email => email);
|
||||||
this.armor = armor;
|
this.armor = armor;
|
||||||
this.askDelete = ko.observable(false);
|
this.askDelete = ko.observable(false);
|
||||||
this.openForDeletion = ko.observable(null).askDeleteHelper();
|
this.openForDeletion = ko.observable(null).askDeleteHelper();
|
||||||
|
|
@ -87,6 +95,18 @@ class OpenPgpKeyModel {
|
||||||
// key.getPrimaryUser()
|
// key.getPrimaryUser()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
get id() { return this.key.getKeyID().toHex().toUpperCase(); }
|
||||||
|
get fingerprint() { return this.key.getFingerprint(); }
|
||||||
|
get can_encrypt() { return !!this.key.getEncryptionKey(); }
|
||||||
|
get can_sign() { return !!this.key.getSigningKey(); }
|
||||||
|
get emails() { return this.key.users.map(user => IDN.toASCII(user.userID.email)).filter(email => email); }
|
||||||
|
get armor() { return this.key.armor(); }
|
||||||
|
*/
|
||||||
|
for(email) {
|
||||||
|
return this.emails.includes(IDN.toASCII(email));
|
||||||
|
}
|
||||||
|
|
||||||
view() {
|
view() {
|
||||||
showScreenPopup(OpenPgpKeyPopupView, [this]);
|
showScreenPopup(OpenPgpKeyPopupView, [this]);
|
||||||
}
|
}
|
||||||
|
|
@ -116,37 +136,62 @@ export const OpenPGPUserStore = new class {
|
||||||
}
|
}
|
||||||
|
|
||||||
loadKeyrings() {
|
loadKeyrings() {
|
||||||
if (window.openpgp) {
|
if (loaded()) {
|
||||||
loadOpenPgpKeys(publicKeysItem).then(keys => {
|
loadOpenPgpKeys(publicKeysItem)
|
||||||
this.publicKeys(keys || []);
|
.then(keys => {
|
||||||
|
this.publicKeys(dedup(keys));
|
||||||
console.log('openpgp.js public keys loaded');
|
console.log('openpgp.js public keys loaded');
|
||||||
});
|
})
|
||||||
loadOpenPgpKeys(privateKeysItem).then(keys => {
|
.finally(() => {
|
||||||
this.privateKeys(keys || [])
|
loadOpenPgpKeys(privateKeysItem)
|
||||||
console.log('openpgp.js private keys loaded');
|
.then(keys => {
|
||||||
|
this.privateKeys(dedup(keys));
|
||||||
|
console.log('openpgp.js private keys loaded');
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
/*SettingsGet('loadBackupKeys') && */this.loadBackupKeys();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
loadBackupKeys() {
|
||||||
|
Remote.request('GetPGPKeys',
|
||||||
|
(iError, oData) => !iError && oData.Result && this.importKeys(oData.Result)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
isSupported() {
|
isSupported() {
|
||||||
return !!window.openpgp;
|
return loaded();
|
||||||
}
|
}
|
||||||
|
|
||||||
importKey(armoredKey) {
|
importKey(armoredKey) {
|
||||||
window.openpgp && openpgp.readKey({armoredKey:armoredKey}).then(key => {
|
this.importKeys([armoredKey]);
|
||||||
if (!key.err) {
|
}
|
||||||
if (key.isPrivate()) {
|
|
||||||
this.privateKeys.push(new OpenPgpKeyModel(armoredKey, key));
|
async importKeys(keys) {
|
||||||
storeOpenPgpKeys(this.privateKeys, privateKeysItem);
|
if (loaded()) {
|
||||||
} else {
|
const privateKeys = this.privateKeys(),
|
||||||
this.publicKeys.push(new OpenPgpKeyModel(armoredKey, key));
|
publicKeys = this.publicKeys();
|
||||||
storeOpenPgpKeys(this.publicKeys, publicKeysItem);
|
for (const armoredKey of keys) try {
|
||||||
|
let key = await openpgp.readKey({armoredKey:armoredKey});
|
||||||
|
if (!key.err) {
|
||||||
|
key = new OpenPgpKeyModel(armoredKey, key);
|
||||||
|
const keys = key.key.isPrivate() ? privateKeys : publicKeys;
|
||||||
|
keys.find(entry => entry.fingerprint == key.fingerprint)
|
||||||
|
|| keys.push(key);
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e, armoredKey);
|
||||||
}
|
}
|
||||||
});
|
this.privateKeys(sort(privateKeys));
|
||||||
|
this.publicKeys(sort(publicKeys));
|
||||||
|
storeOpenPgpKeys(privateKeys, privateKeysItem);
|
||||||
|
storeOpenPgpKeys(publicKeys, publicKeysItem);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -155,7 +200,7 @@ export const OpenPGPUserStore = new class {
|
||||||
keyPair.revocationCertificate
|
keyPair.revocationCertificate
|
||||||
*/
|
*/
|
||||||
storeKeyPair(keyPair) {
|
storeKeyPair(keyPair) {
|
||||||
if (window.openpgp) {
|
if (loaded()) {
|
||||||
openpgp.readKey({armoredKey:keyPair.publicKey}).then(key => {
|
openpgp.readKey({armoredKey:keyPair.publicKey}).then(key => {
|
||||||
this.publicKeys.push(new OpenPgpKeyModel(keyPair.publicKey, key));
|
this.publicKeys.push(new OpenPgpKeyModel(keyPair.publicKey, key));
|
||||||
storeOpenPgpKeys(this.publicKeys, publicKeysItem);
|
storeOpenPgpKeys(this.publicKeys, publicKeysItem);
|
||||||
|
|
@ -173,7 +218,7 @@ export const OpenPGPUserStore = new class {
|
||||||
hasPublicKeyForEmails(recipients) {
|
hasPublicKeyForEmails(recipients) {
|
||||||
const count = recipients.length,
|
const count = recipients.length,
|
||||||
length = count ? recipients.filter(email =>
|
length = count ? recipients.filter(email =>
|
||||||
this.publicKeys().find(key => key.emails.includes(email))
|
this.publicKeys().find(key => key.for(email))
|
||||||
).length : 0;
|
).length : 0;
|
||||||
return length && length === count;
|
return length && length === count;
|
||||||
}
|
}
|
||||||
|
|
@ -201,7 +246,7 @@ export const OpenPGPUserStore = new class {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (privateKey) try {
|
if (privateKey) try {
|
||||||
const decryptedKey = await decryptKey(privateKey, 'BUTTON_DECRYPT');
|
const decryptedKey = await decryptKey(privateKey, 'DECRYPT');
|
||||||
if (decryptedKey) {
|
if (decryptedKey) {
|
||||||
const publicKey = findOpenPGPKey(this.publicKeys, sender/*, sign*/);
|
const publicKey = findOpenPGPKey(this.publicKeys, sender/*, sign*/);
|
||||||
return await openpgp.decrypt({
|
return await openpgp.decrypt({
|
||||||
|
|
@ -222,15 +267,15 @@ export const OpenPGPUserStore = new class {
|
||||||
* https://docs.openpgpjs.org/#sign-and-verify-cleartext-messages
|
* https://docs.openpgpjs.org/#sign-and-verify-cleartext-messages
|
||||||
*/
|
*/
|
||||||
async verify(message) {
|
async verify(message) {
|
||||||
const data = message.pgpSigned(), // { bodyPartId: "1", sigPartId: "2", micAlg: "pgp-sha256" }
|
const data = message.pgpSigned(), // { partId: "1", sigPartId: "2", micAlg: "pgp-sha256" }
|
||||||
publicKey = this.publicKeys().find(key => key.emails.includes(message.from[0].email));
|
publicKey = this.publicKeys().find(key => key.for(message.from[0].email));
|
||||||
if (data && publicKey) {
|
if (data && publicKey) {
|
||||||
data.folder = message.folder;
|
data.folder = message.folder;
|
||||||
data.uid = message.uid;
|
data.uid = message.uid;
|
||||||
data.tryGnuPG = 0;
|
data.tryGnuPG = 0;
|
||||||
let response;
|
let response;
|
||||||
if (data.sigPartId) {
|
if (data.sigPartId) {
|
||||||
response = await Remote.post('MessagePgpVerify', null, data);
|
response = await Remote.post('PgpVerifyMessage', null, data);
|
||||||
} else if (data.bodyPart) {
|
} else if (data.bodyPart) {
|
||||||
// MimePart
|
// MimePart
|
||||||
response = { Result: { text: data.bodyPart.raw, signature: data.sigPart.body } };
|
response = { Result: { text: data.bodyPart.raw, signature: data.sigPart.body } };
|
||||||
|
|
@ -282,7 +327,7 @@ export const OpenPGPUserStore = new class {
|
||||||
*/
|
*/
|
||||||
async encrypt(text, recipients, signPrivateKey) {
|
async encrypt(text, recipients, signPrivateKey) {
|
||||||
const count = recipients.length;
|
const count = recipients.length;
|
||||||
recipients = recipients.map(email => this.publicKeys().find(key => key.emails.includes(email))).filter(key => key);
|
recipients = recipients.map(email => this.publicKeys().find(key => key.for(email))).filter(key => key);
|
||||||
if (count === recipients.length) {
|
if (count === recipients.length) {
|
||||||
if (signPrivateKey) {
|
if (signPrivateKey) {
|
||||||
signPrivateKey = await decryptKey(signPrivateKey);
|
signPrivateKey = await decryptKey(signPrivateKey);
|
||||||
|
|
|
||||||
|
|
@ -8,14 +8,16 @@ import { SettingsCapa, SettingsGet } from 'Common/Globals';
|
||||||
|
|
||||||
import { GnuPGUserStore } from 'Stores/User/GnuPG';
|
import { GnuPGUserStore } from 'Stores/User/GnuPG';
|
||||||
import { OpenPGPUserStore } from 'Stores/User/OpenPGP';
|
import { OpenPGPUserStore } from 'Stores/User/OpenPGP';
|
||||||
|
import { MailvelopeUserStore } from 'Stores/User/Mailvelope';
|
||||||
|
|
||||||
// https://mailvelope.github.io/mailvelope/Keyring.html
|
import Remote from 'Remote/User/Fetch';
|
||||||
let mailvelopeKeyring = null;
|
|
||||||
|
|
||||||
export const
|
export const
|
||||||
BEGIN_PGP_MESSAGE = '-----BEGIN PGP MESSAGE-----',
|
BEGIN_PGP_MESSAGE = '-----BEGIN PGP MESSAGE-----',
|
||||||
// BEGIN_PGP_SIGNATURE = '-----BEGIN PGP SIGNATURE-----',
|
// BEGIN_PGP_SIGNATURE = '-----BEGIN PGP SIGNATURE-----',
|
||||||
// BEGIN_PGP_SIGNED = '-----BEGIN PGP SIGNED MESSAGE-----',
|
// BEGIN_PGP_SIGNED = '-----BEGIN PGP SIGNED MESSAGE-----',
|
||||||
|
// BEGIN_PGP_PUBLIC_KEY = '-----BEGIN PGP PUBLIC KEY BLOCK-----',
|
||||||
|
// END_PGP_PUBLIC_KEY = '-----END PGP PUBLIC KEY BLOCK-----',
|
||||||
|
|
||||||
PgpUserStore = new class {
|
PgpUserStore = new class {
|
||||||
init() {
|
init() {
|
||||||
|
|
@ -33,32 +35,9 @@ export const
|
||||||
}
|
}
|
||||||
|
|
||||||
loadKeyrings(identifier) {
|
loadKeyrings(identifier) {
|
||||||
identifier = identifier || SettingsGet('Email');
|
MailvelopeUserStore.loadKeyring(identifier);
|
||||||
if (window.mailvelope) {
|
|
||||||
const fn = keyring => {
|
|
||||||
mailvelopeKeyring = keyring;
|
|
||||||
console.log('mailvelope ready');
|
|
||||||
};
|
|
||||||
mailvelope.getKeyring().then(fn, err => {
|
|
||||||
if (identifier) {
|
|
||||||
// attempt to create a new keyring for this app/user
|
|
||||||
mailvelope.createKeyring(identifier).then(fn, err => console.error(err));
|
|
||||||
} else {
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
addEventListener('mailvelope-disconnect', event => {
|
|
||||||
alert('Mailvelope is updated to version ' + event.detail.version + '. Reload page');
|
|
||||||
}, false);
|
|
||||||
} else {
|
|
||||||
addEventListener('mailvelope', () => this.loadKeyrings(identifier));
|
|
||||||
}
|
|
||||||
|
|
||||||
OpenPGPUserStore.loadKeyrings();
|
OpenPGPUserStore.loadKeyrings();
|
||||||
|
GnuPGUserStore.loadKeyrings();
|
||||||
if (SettingsCapa('GnuPG')) {
|
|
||||||
GnuPGUserStore.loadKeyrings();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -75,21 +54,28 @@ export const
|
||||||
return 0 === text.trim().indexOf(BEGIN_PGP_MESSAGE);
|
return 0 === text.trim().indexOf(BEGIN_PGP_MESSAGE);
|
||||||
}
|
}
|
||||||
|
|
||||||
async mailvelopeHasPublicKeyForEmails(recipients) {
|
importKey(key, gnuPG, backup) {
|
||||||
const
|
if (gnuPG || backup) {
|
||||||
mailvelope = mailvelopeKeyring && await mailvelopeKeyring.validKeyForAddress(recipients)
|
Remote.request('PgpImportKey',
|
||||||
/*.then(LookupResult => Object.entries(LookupResult))*/,
|
(iError, oData) => {
|
||||||
entries = mailvelope && Object.entries(mailvelope);
|
if (gnuPG && oData?.Result/* && (oData.Result.imported || oData.Result.secretimported)*/) {
|
||||||
return entries && entries.filter(value => value[1]).length === recipients.length;
|
GnuPGUserStore.loadKeyrings();
|
||||||
|
}
|
||||||
|
iError && alert(oData.ErrorMessage);
|
||||||
|
}, {
|
||||||
|
key, gnuPG, backup
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
OpenPGPUserStore.importKey(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if verifying/encrypting a message is possible with given email addresses.
|
* Checks if verifying/encrypting a message is possible with given email addresses.
|
||||||
* Returns the first library that can.
|
* Returns the first library that can.
|
||||||
*/
|
*/
|
||||||
async hasPublicKeyForEmails(recipients) {
|
hasPublicKeyForEmails(recipients) {
|
||||||
const count = recipients.length;
|
if (recipients.length) {
|
||||||
if (count) {
|
|
||||||
if (GnuPGUserStore.hasPublicKeyForEmails(recipients)) {
|
if (GnuPGUserStore.hasPublicKeyForEmails(recipients)) {
|
||||||
return 'gnupg';
|
return 'gnupg';
|
||||||
}
|
}
|
||||||
|
|
@ -100,40 +86,15 @@ export const
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getMailvelopePrivateKeyFor(email/*, sign*/) {
|
|
||||||
if (mailvelopeKeyring && await mailvelopeKeyring.hasPrivateKey({email:email})) {
|
|
||||||
return ['mailvelope', email];
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if signing a message is possible with given email address.
|
|
||||||
* Returns the first library that can.
|
|
||||||
*/
|
|
||||||
async getKeyForSigning(email) {
|
|
||||||
let key = OpenPGPUserStore.getPrivateKeyFor(email, 1);
|
|
||||||
if (key) {
|
|
||||||
return ['openpgp', key];
|
|
||||||
}
|
|
||||||
|
|
||||||
key = GnuPGUserStore.getPrivateKeyFor(email, 1);
|
|
||||||
if (key) {
|
|
||||||
return ['gnupg', key];
|
|
||||||
}
|
|
||||||
|
|
||||||
// return await this.getMailvelopePrivateKeyFor(email, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
async decrypt(message) {
|
async decrypt(message) {
|
||||||
const sender = message.from[0].email,
|
const armoredText = message.plain();
|
||||||
armoredText = message.plain();
|
|
||||||
if (!this.isEncrypted(armoredText)) {
|
if (!this.isEncrypted(armoredText)) {
|
||||||
throw Error('Not armored text');
|
throw Error('Not armored text');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try OpenPGP.js
|
// Try OpenPGP.js
|
||||||
if (OpenPGPUserStore.isSupported()) {
|
if (OpenPGPUserStore.isSupported()) {
|
||||||
|
const sender = message.from[0].email;
|
||||||
let result = await OpenPGPUserStore.decrypt(armoredText, sender);
|
let result = await OpenPGPUserStore.decrypt(armoredText, sender);
|
||||||
if (result) {
|
if (result) {
|
||||||
return result;
|
return result;
|
||||||
|
|
@ -141,68 +102,20 @@ export const
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try Mailvelope (does not support inline images)
|
// Try Mailvelope (does not support inline images)
|
||||||
if (mailvelopeKeyring) {
|
return (await MailvelopeUserStore.decrypt(message))
|
||||||
try {
|
// Or try GnuPG
|
||||||
let emails = [...message.from,...message.to,...message.cc].validUnique(),
|
|| GnuPGUserStore.decrypt(message);
|
||||||
i = emails.length;
|
|
||||||
while (i--) {
|
|
||||||
if (await this.getMailvelopePrivateKeyFor(emails[i].email)) {
|
|
||||||
/**
|
|
||||||
* https://mailvelope.github.io/mailvelope/Mailvelope.html#createEncryptedFormContainer
|
|
||||||
* Creates an iframe to display an encrypted form
|
|
||||||
*/
|
|
||||||
// mailvelope.createEncryptedFormContainer('#mailvelope-form');
|
|
||||||
/**
|
|
||||||
* https://mailvelope.github.io/mailvelope/Mailvelope.html#createDisplayContainer
|
|
||||||
* Creates an iframe to display the decrypted content of the encrypted mail.
|
|
||||||
*/
|
|
||||||
const body = message.body;
|
|
||||||
body.textContent = '';
|
|
||||||
let result = await mailvelope.createDisplayContainer(
|
|
||||||
'#'+body.id,
|
|
||||||
armoredText,
|
|
||||||
mailvelopeKeyring,
|
|
||||||
{
|
|
||||||
senderAddress: sender
|
|
||||||
// emails[i].email
|
|
||||||
}
|
|
||||||
);
|
|
||||||
if (result) {
|
|
||||||
if (result.error?.message) {
|
|
||||||
if ('PWD_DIALOG_CANCEL' !== result.error.code) {
|
|
||||||
alert(result.error.code + ': ' + result.error.message);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
body.classList.add('mailvelope');
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now try GnuPG
|
|
||||||
return GnuPGUserStore.decrypt(message);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async verify(message) {
|
async verify(message) {
|
||||||
const signed = message.pgpSigned();
|
const signed = message.pgpSigned(),
|
||||||
|
sender = message.from[0].email;
|
||||||
if (signed) {
|
if (signed) {
|
||||||
const sender = message.from[0].email,
|
// OpenPGP only when inline, else we must download the whole message
|
||||||
gnupg = GnuPGUserStore.hasPublicKeyForEmails([sender]),
|
if (!signed.sigPartId && OpenPGPUserStore.hasPublicKeyForEmails([sender])) {
|
||||||
openpgp = OpenPGPUserStore.hasPublicKeyForEmails([sender]);
|
|
||||||
// Detached signature use GnuPG first, else we must download whole message
|
|
||||||
if (gnupg && signed.sigPartId) {
|
|
||||||
return GnuPGUserStore.verify(message);
|
|
||||||
}
|
|
||||||
if (openpgp) {
|
|
||||||
return OpenPGPUserStore.verify(message);
|
return OpenPGPUserStore.verify(message);
|
||||||
}
|
}
|
||||||
if (gnupg) {
|
if (GnuPGUserStore.hasPublicKeyForEmails([sender])) {
|
||||||
return GnuPGUserStore.verify(message);
|
return GnuPGUserStore.verify(message);
|
||||||
}
|
}
|
||||||
// Mailvelope can't
|
// Mailvelope can't
|
||||||
|
|
@ -210,29 +123,23 @@ export const
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
getPublicKeyOfEmails(recipients) {
|
||||||
* Returns headers that should be added to an outgoing email.
|
if (recipients.length) {
|
||||||
* So far this is only the autocrypt header.
|
let result = {};
|
||||||
*/
|
recipients.forEach(email => {
|
||||||
/*
|
OpenPGPUserStore.publicKeys().forEach(key => {
|
||||||
mailvelopeKeyring.additionalHeadersForOutgoingEmail(headers)
|
if (key.for(email)) {
|
||||||
mailvelopeKeyring.addSyncHandler(syncHandlerObj)
|
result[email] = key.armor;
|
||||||
mailvelopeKeyring.createKeyBackupContainer(selector, options)
|
}
|
||||||
mailvelopeKeyring.createKeyGenContainer(selector, {
|
});
|
||||||
// userIds: [],
|
GnuPGUserStore.publicKeys.map(async key => {
|
||||||
keySize: 4096
|
if (!result[email] && key.for(email)) {
|
||||||
})
|
result[email] = await key.fetch();
|
||||||
|
}
|
||||||
mailvelopeKeyring.exportOwnPublicKey(emailAddr).then(<AsciiArmored, Error>)
|
});
|
||||||
mailvelopeKeyring.importPublicKey(armored)
|
});
|
||||||
|
return result;
|
||||||
// https://mailvelope.github.io/mailvelope/global.html#SyncHandlerObject
|
}
|
||||||
mailvelopeKeyring.addSyncHandler({
|
return false;
|
||||||
uploadSync
|
}
|
||||||
downloadSync
|
|
||||||
backup
|
|
||||||
restore
|
|
||||||
});
|
|
||||||
*/
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
21
dev/Stores/User/SMime.js
Normal file
21
dev/Stores/User/SMime.js
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { addObservablesTo, koArrayWithDestroy } from 'External/ko';
|
||||||
|
import { baseCollator } from 'Common/Translator';
|
||||||
|
import Remote from 'Remote/User/Fetch';
|
||||||
|
|
||||||
|
export const SMimeUserStore = koArrayWithDestroy();
|
||||||
|
|
||||||
|
addObservablesTo(SMimeUserStore, {
|
||||||
|
loading: false
|
||||||
|
});
|
||||||
|
|
||||||
|
SMimeUserStore.loadCertificates = () => {
|
||||||
|
SMimeUserStore([]);
|
||||||
|
SMimeUserStore.loading(true);
|
||||||
|
Remote.request('SMimeGetCertificates', (iError, oData) => {
|
||||||
|
SMimeUserStore.loading(false);
|
||||||
|
const collator = baseCollator();
|
||||||
|
iError || SMimeUserStore(oData.Result.sort(
|
||||||
|
(a, b) => collator.compare(a.emailAddress, b.emailAddress) || (b.validTo_time_t - a.validTo_time_t)
|
||||||
|
));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
@ -29,6 +29,7 @@ export const SettingsUserStore = new class {
|
||||||
showNextMessage: 0,
|
showNextMessage: 0,
|
||||||
allowDraftAutosave: 1,
|
allowDraftAutosave: 1,
|
||||||
useThreads: 0,
|
useThreads: 0,
|
||||||
|
threadAlgorithm: '',
|
||||||
replySameFolder: 0,
|
replySameFolder: 0,
|
||||||
hideUnsubscribed: 0,
|
hideUnsubscribed: 0,
|
||||||
hideDeleted: 1,
|
hideDeleted: 1,
|
||||||
|
|
@ -47,6 +48,7 @@ export const SettingsUserStore = new class {
|
||||||
|
|
||||||
layout: 1,
|
layout: 1,
|
||||||
editorDefaultType: 'Html',
|
editorDefaultType: 'Html',
|
||||||
|
editorWysiwyg: 'Squire',
|
||||||
msgDefaultAction: 1
|
msgDefaultAction: 1
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -78,9 +80,10 @@ export const SettingsUserStore = new class {
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
const self = this;
|
const self = this;
|
||||||
self.editorDefaultType(SettingsGet('EditorDefaultType'));
|
|
||||||
|
|
||||||
[
|
[
|
||||||
|
'EditorDefaultType',
|
||||||
|
'editorWysiwyg',
|
||||||
'messageNewWindow',
|
'messageNewWindow',
|
||||||
'messageReadAuto',
|
'messageReadAuto',
|
||||||
'MsgDefaultAction',
|
'MsgDefaultAction',
|
||||||
|
|
@ -97,7 +100,8 @@ export const SettingsUserStore = new class {
|
||||||
'listGrouped',
|
'listGrouped',
|
||||||
'showNextMessage',
|
'showNextMessage',
|
||||||
'AllowDraftAutosave',
|
'AllowDraftAutosave',
|
||||||
'UseThreads',
|
'useThreads',
|
||||||
|
'threadAlgorithm',
|
||||||
'ReplySameFolder',
|
'ReplySameFolder',
|
||||||
'HideUnsubscribed',
|
'HideUnsubscribed',
|
||||||
'HideDeleted',
|
'HideDeleted',
|
||||||
|
|
@ -109,6 +113,39 @@ export const SettingsUserStore = new class {
|
||||||
'pgpSign',
|
'pgpSign',
|
||||||
'pgpEncrypt',
|
'pgpEncrypt',
|
||||||
'allowSpellcheck'
|
'allowSpellcheck'
|
||||||
|
/*
|
||||||
|
'MessagesPerPage',
|
||||||
|
'MessageReadDelay',
|
||||||
|
'SoundNotification',
|
||||||
|
'NotificationSound',
|
||||||
|
'DesktopNotifications',
|
||||||
|
'Layout',
|
||||||
|
'AutoLogout',
|
||||||
|
'ContactsAutosave',
|
||||||
|
'contactsAllowed',
|
||||||
|
'CheckMailInterval',
|
||||||
|
'SentFolder',
|
||||||
|
'DraftsFolder',
|
||||||
|
'JunkFolder',
|
||||||
|
'TrashFolder',
|
||||||
|
'ArchiveFolder',
|
||||||
|
'hourCycle',
|
||||||
|
'Resizer4Width',
|
||||||
|
'Resizer5Width',
|
||||||
|
'Resizer5Height',
|
||||||
|
'fontSansSerif',
|
||||||
|
'fontSerif',
|
||||||
|
'fontMono',
|
||||||
|
'userBackgroundName',
|
||||||
|
'userBackgroundHash',
|
||||||
|
'autoVerifySignatures',
|
||||||
|
'allowLanguagesOnSettings',
|
||||||
|
'attachmentLimit',
|
||||||
|
'Theme',
|
||||||
|
'language',
|
||||||
|
'clientLanguage',
|
||||||
|
'StaticLibsJs',
|
||||||
|
*/
|
||||||
].forEach(name => {
|
].forEach(name => {
|
||||||
let value = SettingsGet(name);
|
let value = SettingsGet(name);
|
||||||
name = name[0].toLowerCase() + name.slice(1);
|
name = name[0].toLowerCase() + name.slice(1);
|
||||||
|
|
|
||||||
|
|
@ -7,16 +7,12 @@
|
||||||
max-width: 810px;
|
max-width: 810px;
|
||||||
|
|
||||||
.domain-desc {
|
.domain-desc {
|
||||||
color: #666;
|
|
||||||
line-height: 20px;
|
|
||||||
background-color: #f9f9f9;
|
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
border: 1px solid #eee;
|
border: 1px solid;
|
||||||
border-radius: var(--input-border-radius, 3px);
|
border-radius: var(--input-border-radius, 3px);
|
||||||
margin: -5px 0;
|
margin: -5px 0;
|
||||||
i {
|
strong {
|
||||||
font-style: normal;
|
color: var(--error-clr, #b94a48);
|
||||||
color: red;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,15 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* https://github.com/the-djmaze/snappymail/issues/1427 */
|
||||||
|
#V-MailFolderList .e-checkbox {
|
||||||
|
margin: 0 4px;
|
||||||
|
width: calc(100% - 8px);
|
||||||
|
}
|
||||||
|
#V-MailFolderList .unreadOnly li a:not(.unread-sub):not([data-unread]) {
|
||||||
|
display:none;
|
||||||
|
}
|
||||||
|
|
||||||
.b-folders {
|
.b-folders {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
@ -56,6 +65,7 @@
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
min-width: 100px;
|
min-width: 100px;
|
||||||
|
color: var(--folders-color, #333);
|
||||||
}
|
}
|
||||||
|
|
||||||
hr {
|
hr {
|
||||||
|
|
@ -68,22 +78,18 @@
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|
||||||
a {
|
a {
|
||||||
display: block;
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
line-height: 34px;
|
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
vertical-align: middle;
|
|
||||||
color: var(--folders-disabled-color, #666);
|
|
||||||
border-left: 3px solid transparent;
|
border-left: 3px solid transparent;
|
||||||
|
color: var(--folders-color, #333);
|
||||||
|
display: block;
|
||||||
|
line-height: 34px;
|
||||||
padding: 0 2em 0 @folderItemPadding;
|
padding: 0 2em 0 @folderItemPadding;
|
||||||
|
position: relative;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
vertical-align: middle;
|
||||||
|
z-index: 1;
|
||||||
|
|
||||||
&.selectable {
|
&.selectable {
|
||||||
|
|
||||||
color: var(--folders-color, #333);
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
|
|
@ -106,6 +112,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
&:not(.selectable) {
|
&:not(.selectable) {
|
||||||
|
color: var(--folders-disabled-color, #666);
|
||||||
cursor: default;
|
cursor: default;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
@ -185,27 +192,6 @@
|
||||||
li li li li li a {
|
li li li li li a {
|
||||||
text-indent: 4em;
|
text-indent: 4em;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**/
|
|
||||||
&.single-root-inbox .b-folders-user > li > a {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.single-root-inbox {
|
|
||||||
li li a {
|
|
||||||
text-indent: 0;
|
|
||||||
}
|
|
||||||
li li li a {
|
|
||||||
text-indent: 1em;
|
|
||||||
}
|
|
||||||
li li li li a {
|
|
||||||
text-indent: 2em;
|
|
||||||
}
|
|
||||||
li li li li li a {
|
|
||||||
text-indent: 3em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/**/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#rl-left .buttonCompose {
|
#rl-left .buttonCompose {
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,15 @@
|
||||||
|
|
||||||
max-width: 770px;
|
max-width: 770px;
|
||||||
|
|
||||||
|
.tab-content {
|
||||||
|
grid-column-end: 5;
|
||||||
|
padding-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
.e-signature-place {
|
.e-signature-place {
|
||||||
border: 1px solid #ccc;
|
border: 1px solid #ccc;
|
||||||
border-radius: var(--input-border-radius, 3px);
|
border-radius: var(--input-border-radius, 3px);
|
||||||
|
margin-bottom: 1.43em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.textEmail {
|
.textEmail {
|
||||||
|
|
|
||||||
|
|
@ -327,6 +327,7 @@
|
||||||
.attachmentParent,
|
.attachmentParent,
|
||||||
.flagParent {
|
.flagParent {
|
||||||
margin: 0 10px 0 5px;
|
margin: 0 10px 0 5px;
|
||||||
|
min-width: 1em;
|
||||||
}
|
}
|
||||||
.flagParent::after {
|
.flagParent::after {
|
||||||
content: '☆'; /*⚐*/
|
content: '☆'; /*⚐*/
|
||||||
|
|
|
||||||
|
|
@ -198,7 +198,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.openpgp-control {
|
.crypto-control {
|
||||||
padding: 0.5em;
|
padding: 0.5em;
|
||||||
|
|
||||||
span {
|
span {
|
||||||
|
|
@ -331,6 +331,11 @@
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
|
&.swapColors {
|
||||||
|
background-color: var(--main-color, currentcolor);
|
||||||
|
color: var(--main-bg-color);
|
||||||
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
box-sizing: unset;
|
box-sizing: unset;
|
||||||
/* unicode-bidi: plaintext;*/
|
/* unicode-bidi: plaintext;*/
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,10 @@
|
||||||
margin-top: 1em;
|
margin-top: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
td:first-child {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
td + td {
|
td + td {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
width: 1em;
|
width: 1em;
|
||||||
|
|
@ -24,10 +28,6 @@
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.folder-name.can-be-edited {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
select {
|
||||||
width: auto;
|
width: auto;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,14 +9,14 @@
|
||||||
color: red;
|
color: red;
|
||||||
content: attr(data-rainloopErrorTip);
|
content: attr(data-rainloopErrorTip);
|
||||||
font-size: 90%;
|
font-size: 90%;
|
||||||
left: 100%;
|
left: 0;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
padding: 0.25em;
|
padding: 0.25em;
|
||||||
position : absolute;
|
position : absolute;
|
||||||
text-align: var(--left, left);
|
text-align: var(--left, left);
|
||||||
top: 100%;
|
top: 100%;
|
||||||
transition: opacity 0.2s ease-in-out;
|
transition: opacity 0.2s ease-in-out;
|
||||||
white-space: nowrap;
|
white-space: pre;
|
||||||
z-index: 2001;
|
z-index: 2001;
|
||||||
}
|
}
|
||||||
@media screen and (max-width: @maxMobileWidth) {
|
@media screen and (max-width: @maxMobileWidth) {
|
||||||
|
|
|
||||||
|
|
@ -123,4 +123,8 @@ html[dir="rtl"] {
|
||||||
border-right: 6px solid #eee;
|
border-right: 6px solid #eee;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[data-rainloopErrorTip]::before {
|
||||||
|
left: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { addObservablesTo } from 'External/ko';
|
import { addObservablesTo } from 'External/ko';
|
||||||
import { getNotification } from 'Common/Translator';
|
import { getNotification } from 'Common/Translator';
|
||||||
|
import { loadAccountsAndIdentities } from 'Common/UtilsUser';
|
||||||
|
|
||||||
import Remote from 'Remote/User/Fetch';
|
import Remote from 'Remote/User/Fetch';
|
||||||
|
|
||||||
|
|
@ -37,7 +38,7 @@ export class AccountPopupView extends AbstractViewPopup {
|
||||||
this.submitError(getNotification(iError));
|
this.submitError(getNotification(iError));
|
||||||
this.submitErrorAdditional(data?.ErrorMessageAdditional);
|
this.submitErrorAdditional(data?.ErrorMessageAdditional);
|
||||||
} else {
|
} else {
|
||||||
rl.app.accountsAndIdentities();
|
loadAccountsAndIdentities();
|
||||||
this.close();
|
this.close();
|
||||||
}
|
}
|
||||||
}, data
|
}, data
|
||||||
|
|
|
||||||
|
|
@ -32,14 +32,12 @@ export class AdvancedSearchPopupView extends AbstractViewPopup {
|
||||||
// Almost the same as MessageModel.tagOptions
|
// Almost the same as MessageModel.tagOptions
|
||||||
keywords: () => {
|
keywords: () => {
|
||||||
const keywords = [{value:'',label:''}];
|
const keywords = [{value:'',label:''}];
|
||||||
FolderUserStore.currentFolder().permanentFlags.forEach(value => {
|
FolderUserStore.currentFolder().optionalTags().forEach(value => {
|
||||||
if (isAllowedKeyword(value)) {
|
let lower = value.toLowerCase();
|
||||||
let lower = value.toLowerCase();
|
keywords.push({
|
||||||
keywords.push({
|
value: value,
|
||||||
value: value,
|
label: i18n('MESSAGE_TAGS/'+lower, 0, lower)
|
||||||
label: i18n('MESSAGE_TAGS/'+lower, 0, lower)
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
return keywords
|
return keywords
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -85,29 +85,3 @@ export class AskPopupView extends AbstractViewPopup {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
AskPopupView.password = function(sAskDesc, btnText) {
|
|
||||||
return new Promise(resolve => {
|
|
||||||
this.showModal([
|
|
||||||
sAskDesc,
|
|
||||||
view => resolve({password:view.passphrase(), remember:view.remember()}),
|
|
||||||
() => resolve(null),
|
|
||||||
true,
|
|
||||||
5,
|
|
||||||
btnText
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
AskPopupView.credentials = function(sAskDesc, btnText) {
|
|
||||||
return new Promise(resolve => {
|
|
||||||
this.showModal([
|
|
||||||
sAskDesc,
|
|
||||||
view => resolve({username:view.username(), password:view.passphrase(), remember:view.remember()}),
|
|
||||||
() => resolve(null),
|
|
||||||
true,
|
|
||||||
3,
|
|
||||||
btnText
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,8 @@ import {
|
||||||
} from 'Common/EnumsUser';
|
} from 'Common/EnumsUser';
|
||||||
|
|
||||||
import { pInt, isArray, arrayLength, b64Encode } from 'Common/Utils';
|
import { pInt, isArray, arrayLength, b64Encode } from 'Common/Utils';
|
||||||
import { encodeHtml, HtmlEditor, htmlToPlain } from 'Common/Html';
|
import { encodeHtml, htmlToPlain } from 'Common/Html';
|
||||||
|
import { HtmlEditor } from 'Common/HtmlEditor';
|
||||||
import { koArrayWithDestroy, addObservablesTo, addComputablesTo, addSubscribablesTo } from 'External/ko';
|
import { koArrayWithDestroy, addObservablesTo, addComputablesTo, addSubscribablesTo } from 'External/ko';
|
||||||
|
|
||||||
import { UNUSED_OPTION_VALUE } from 'Common/Consts';
|
import { UNUSED_OPTION_VALUE } from 'Common/Consts';
|
||||||
|
|
@ -27,16 +28,24 @@ import { SettingsUserStore } from 'Stores/User/Settings';
|
||||||
import { IdentityUserStore } from 'Stores/User/Identity';
|
import { IdentityUserStore } from 'Stores/User/Identity';
|
||||||
import { AccountUserStore } from 'Stores/User/Account';
|
import { AccountUserStore } from 'Stores/User/Account';
|
||||||
import { FolderUserStore } from 'Stores/User/Folder';
|
import { FolderUserStore } from 'Stores/User/Folder';
|
||||||
|
|
||||||
import { PgpUserStore } from 'Stores/User/Pgp';
|
import { PgpUserStore } from 'Stores/User/Pgp';
|
||||||
import { OpenPGPUserStore } from 'Stores/User/OpenPGP';
|
import { OpenPGPUserStore } from 'Stores/User/OpenPGP';
|
||||||
import { GnuPGUserStore } from 'Stores/User/GnuPG';
|
import { GnuPGUserStore } from 'Stores/User/GnuPG';
|
||||||
|
import { MailvelopeUserStore } from 'Stores/User/Mailvelope';
|
||||||
|
//import { OpenPgpImportPopupView } from 'View/Popup/OpenPgpImport';
|
||||||
|
import { SMimeUserStore } from 'Stores/User/SMime';
|
||||||
|
import { Passphrases } from 'Storage/Passphrases';
|
||||||
|
|
||||||
import { MessageUserStore } from 'Stores/User/Message';
|
import { MessageUserStore } from 'Stores/User/Message';
|
||||||
import { MessagelistUserStore } from 'Stores/User/Messagelist';
|
import { MessagelistUserStore } from 'Stores/User/Messagelist';
|
||||||
|
|
||||||
import Remote from 'Remote/User/Fetch';
|
import Remote from 'Remote/User/Fetch';
|
||||||
|
|
||||||
import { ComposeAttachmentModel } from 'Model/ComposeAttachment';
|
import { ComposeAttachmentModel } from 'Model/ComposeAttachment';
|
||||||
import { EmailModel, addressparser } from 'Model/Email';
|
import { EmailModel } from 'Model/Email';
|
||||||
|
import { MimeHeaderAutocryptModel } from 'Model/MimeHeaderAutocrypt';
|
||||||
|
import { addressparser } from 'Mime/Address';
|
||||||
|
|
||||||
import { decorateKoCommands, showScreenPopup } from 'Knoin/Knoin';
|
import { decorateKoCommands, showScreenPopup } from 'Knoin/Knoin';
|
||||||
import { AbstractViewPopup } from 'Knoin/AbstractViews';
|
import { AbstractViewPopup } from 'Knoin/AbstractViews';
|
||||||
|
|
@ -44,6 +53,7 @@ import { AbstractViewPopup } from 'Knoin/AbstractViews';
|
||||||
import { FolderSystemPopupView } from 'View/Popup/FolderSystem';
|
import { FolderSystemPopupView } from 'View/Popup/FolderSystem';
|
||||||
import { AskPopupView } from 'View/Popup/Ask';
|
import { AskPopupView } from 'View/Popup/Ask';
|
||||||
import { ContactsPopupView } from 'View/Popup/Contacts';
|
import { ContactsPopupView } from 'View/Popup/Contacts';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
import { ThemeStore } from 'Stores/Theme';
|
import { ThemeStore } from 'Stores/Theme';
|
||||||
|
|
||||||
|
|
@ -56,7 +66,7 @@ const
|
||||||
|
|
||||||
tpl = createElement('template'),
|
tpl = createElement('template'),
|
||||||
|
|
||||||
base64_encode = text => b64Encode(text).match(/.{1,76}/g).join('\r\n'),
|
base64_encode = text => text ? b64Encode(text).match(/.{1,76}/g).join('\r\n') : '',
|
||||||
|
|
||||||
getEmail = value => addressparser(value)[0]?.email || false,
|
getEmail = value => addressparser(value)[0]?.email || false,
|
||||||
|
|
||||||
|
|
@ -243,11 +253,8 @@ export class ComposePopupView extends AbstractViewPopup {
|
||||||
showBcc: false,
|
showBcc: false,
|
||||||
showReplyTo: false,
|
showReplyTo: false,
|
||||||
|
|
||||||
pgpSign: false,
|
doSign: false,
|
||||||
canPgpSign: false,
|
doEncrypt: false,
|
||||||
pgpEncrypt: false,
|
|
||||||
canPgpEncrypt: false,
|
|
||||||
canMailvelope: false,
|
|
||||||
|
|
||||||
draftsFolder: '',
|
draftsFolder: '',
|
||||||
draftUid: 0,
|
draftUid: 0,
|
||||||
|
|
@ -271,6 +278,8 @@ export class ComposePopupView extends AbstractViewPopup {
|
||||||
});
|
});
|
||||||
|
|
||||||
this.attachments = koArrayWithDestroy();
|
this.attachments = koArrayWithDestroy();
|
||||||
|
this.encryptOptions = koArrayWithDestroy();
|
||||||
|
this.signOptions = koArrayWithDestroy();
|
||||||
|
|
||||||
this.dragAndDropOver = ko.observable(false).extend({ debounce: 1 });
|
this.dragAndDropOver = ko.observable(false).extend({ debounce: 1 });
|
||||||
this.dragAndDropVisible = ko.observable(false).extend({ debounce: 1 });
|
this.dragAndDropVisible = ko.observable(false).extend({ debounce: 1 });
|
||||||
|
|
@ -323,6 +332,13 @@ export class ComposePopupView extends AbstractViewPopup {
|
||||||
attachmentsInProcessCount: () => this.attachmentsInProcess.length,
|
attachmentsInProcessCount: () => this.attachmentsInProcess.length,
|
||||||
isDraft: () => this.draftsFolder() && this.draftUid(),
|
isDraft: () => this.draftsFolder() && this.draftUid(),
|
||||||
|
|
||||||
|
canEncrypt: () => this.encryptOptions().length,
|
||||||
|
canMailvelope: () => this.encryptOptions.includes('Mailvelope'),
|
||||||
|
canSign: () => this.signOptions().length,
|
||||||
|
|
||||||
|
encryptOptionsText: () => this.encryptOptions().join(', '),
|
||||||
|
signOptionsText: () => this.signOptions().map(o => o[0]).join(', '),
|
||||||
|
|
||||||
identitiesOptions: () =>
|
identitiesOptions: () =>
|
||||||
IdentityUserStore.map(item => ({
|
IdentityUserStore.map(item => ({
|
||||||
item: item,
|
item: item,
|
||||||
|
|
@ -340,32 +356,31 @@ export class ComposePopupView extends AbstractViewPopup {
|
||||||
|
|
||||||
sendSuccessButSaveError: value => !value && this.savedErrorDesc(''),
|
sendSuccessButSaveError: value => !value && this.savedErrorDesc(''),
|
||||||
|
|
||||||
currentIdentity: value => value && this.from(value.formattedName()),
|
currentIdentity: value => {
|
||||||
|
if (value) {
|
||||||
|
this.from(value.formattedName());
|
||||||
|
this.doEncrypt(value.pgpEncrypt() || SettingsUserStore.pgpEncrypt());
|
||||||
|
this.doSign(value.pgpSign() || SettingsUserStore.pgpSign());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
from: value => {
|
from: () => {
|
||||||
this.canPgpSign(false);
|
this.initSign();
|
||||||
value = getEmail(value);
|
this.initEncrypt();
|
||||||
value && PgpUserStore.getKeyForSigning(value).then(result => {
|
|
||||||
console.log({
|
|
||||||
email: value,
|
|
||||||
canPgpSign:result
|
|
||||||
});
|
|
||||||
this.canPgpSign(result)
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
cc: value => {
|
cc: value => {
|
||||||
if (false === this.showCc() && value.length) {
|
if (false === this.showCc() && value.length) {
|
||||||
this.showCc(true);
|
this.showCc(true);
|
||||||
}
|
}
|
||||||
this.initPgpEncrypt();
|
this.initEncrypt();
|
||||||
},
|
},
|
||||||
|
|
||||||
bcc: value => {
|
bcc: value => {
|
||||||
if (false === this.showBcc() && value.length) {
|
if (false === this.showBcc() && value.length) {
|
||||||
this.showBcc(true);
|
this.showBcc(true);
|
||||||
}
|
}
|
||||||
this.initPgpEncrypt();
|
this.initEncrypt();
|
||||||
},
|
},
|
||||||
|
|
||||||
replyTo: value => {
|
replyTo: value => {
|
||||||
|
|
@ -384,7 +399,7 @@ export class ComposePopupView extends AbstractViewPopup {
|
||||||
if (this.emptyToError() && value.length) {
|
if (this.emptyToError() && value.length) {
|
||||||
this.emptyToError(false);
|
this.emptyToError(false);
|
||||||
}
|
}
|
||||||
this.initPgpEncrypt();
|
this.initEncrypt();
|
||||||
},
|
},
|
||||||
|
|
||||||
attachmentsInProcess: value => {
|
attachmentsInProcess: value => {
|
||||||
|
|
@ -412,7 +427,7 @@ export class ComposePopupView extends AbstractViewPopup {
|
||||||
case 'K': quota *= 1024;
|
case 'K': quota *= 1024;
|
||||||
}
|
}
|
||||||
// Issue: can't select signing key
|
// Issue: can't select signing key
|
||||||
// this.pgpSign(this.pgpSign() || confirm('Sign this message?'));
|
// this.doSign(this.doSign() || confirm('Sign this message?'));
|
||||||
mailvelope.createEditorContainer('#mailvelope-editor', PgpUserStore.mailvelopeKeyring, {
|
mailvelope.createEditorContainer('#mailvelope-editor', PgpUserStore.mailvelopeKeyring, {
|
||||||
// https://mailvelope.github.io/mailvelope/global.html#EditorContainerOptions
|
// https://mailvelope.github.io/mailvelope/global.html#EditorContainerOptions
|
||||||
quota: Math.max(2048, (quota / 1024)) - 48, // (text + attachments) limit in kilobytes
|
quota: Math.max(2048, (quota / 1024)) - 48, // (text + attachments) limit in kilobytes
|
||||||
|
|
@ -424,7 +439,7 @@ export class ComposePopupView extends AbstractViewPopup {
|
||||||
quotedMailHeader: '', // header to be added before the quoted mail
|
quotedMailHeader: '', // header to be added before the quoted mail
|
||||||
keepAttachments: false, // add attachments of quotedMail to editor (default: false)
|
keepAttachments: false, // add attachments of quotedMail to editor (default: false)
|
||||||
// Issue: can't select signing key
|
// Issue: can't select signing key
|
||||||
signMsg: this.pgpSign()
|
signMsg: this.doSign()
|
||||||
*/
|
*/
|
||||||
}).then(editor => this.mailvelope = editor);
|
}).then(editor => this.mailvelope = editor);
|
||||||
}
|
}
|
||||||
|
|
@ -443,7 +458,8 @@ export class ComposePopupView extends AbstractViewPopup {
|
||||||
}
|
}
|
||||||
|
|
||||||
sendCommand() {
|
sendCommand() {
|
||||||
let sSentFolder = FolderUserStore.sentFolder();
|
const identity = this.currentIdentity();
|
||||||
|
let sSentFolder = identity?.sentFolder?.() || FolderUserStore.sentFolder();
|
||||||
|
|
||||||
this.attachmentsInProcessError(false);
|
this.attachmentsInProcessError(false);
|
||||||
this.attachmentsInErrorError(false);
|
this.attachmentsInErrorError(false);
|
||||||
|
|
@ -465,8 +481,7 @@ export class ComposePopupView extends AbstractViewPopup {
|
||||||
if (SettingsUserStore.replySameFolder()) {
|
if (SettingsUserStore.replySameFolder()) {
|
||||||
if (
|
if (
|
||||||
3 === arrayLength(this.aDraftInfo) &&
|
3 === arrayLength(this.aDraftInfo) &&
|
||||||
null != this.aDraftInfo[2] &&
|
this.aDraftInfo[2]?.length
|
||||||
this.aDraftInfo[2].length
|
|
||||||
) {
|
) {
|
||||||
sSentFolder = this.aDraftInfo[2];
|
sSentFolder = this.aDraftInfo[2];
|
||||||
}
|
}
|
||||||
|
|
@ -474,63 +489,91 @@ export class ComposePopupView extends AbstractViewPopup {
|
||||||
|
|
||||||
if (!sSentFolder) {
|
if (!sSentFolder) {
|
||||||
showScreenPopup(FolderSystemPopupView, [FolderType.Sent]);
|
showScreenPopup(FolderSystemPopupView, [FolderType.Sent]);
|
||||||
} else try {
|
} else {
|
||||||
this.sendError(false);
|
const sendError = e => {
|
||||||
this.sending(true);
|
|
||||||
|
|
||||||
sSentFolder = UNUSED_OPTION_VALUE === sSentFolder ? '' : sSentFolder;
|
|
||||||
|
|
||||||
this.getMessageRequestParams(sSentFolder).then(params => {
|
|
||||||
Remote.request('SendMessage',
|
|
||||||
(iError, data) => {
|
|
||||||
this.sending(false);
|
|
||||||
if (iError) {
|
|
||||||
if (Notifications.CantSaveMessage === iError) {
|
|
||||||
this.sendSuccessButSaveError(true);
|
|
||||||
this.savedErrorDesc(i18n('COMPOSE/SAVED_ERROR_ON_SEND').trim());
|
|
||||||
} else {
|
|
||||||
this.sendError(true);
|
|
||||||
this.sendErrorDesc(getNotification(iError, data?.ErrorMessage)
|
|
||||||
|| getNotification(Notifications.CantSendMessage));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (arrayLength(this.aDraftInfo) > 0) {
|
|
||||||
const flag = {
|
|
||||||
'reply': '\\answered',
|
|
||||||
'forward': '$forwarded'
|
|
||||||
}[this.aDraftInfo[0]];
|
|
||||||
if (flag) {
|
|
||||||
const aFlags = oLastMessage.flags();
|
|
||||||
if (aFlags.indexOf(flag) === -1) {
|
|
||||||
aFlags.push(flag);
|
|
||||||
oLastMessage.flags(aFlags);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.close();
|
|
||||||
}
|
|
||||||
setFolderETag(this.draftsFolder(), '');
|
|
||||||
setFolderETag(sSentFolder, '');
|
|
||||||
if (3 === arrayLength(this.aDraftInfo)) {
|
|
||||||
const folder = this.aDraftInfo[2];
|
|
||||||
setFolderETag(folder, '');
|
|
||||||
}
|
|
||||||
reloadDraftFolder();
|
|
||||||
},
|
|
||||||
params,
|
|
||||||
30000
|
|
||||||
);
|
|
||||||
}).catch(e => {
|
|
||||||
console.error(e);
|
console.error(e);
|
||||||
this.sendError(true);
|
this.sendError(true);
|
||||||
this.sendErrorDesc(e);
|
this.sendErrorDesc(e);
|
||||||
this.sending(false);
|
this.sending(false);
|
||||||
});
|
};
|
||||||
} catch (e) {
|
const sendFailed = (iError, data) => {
|
||||||
console.error(e);
|
this.sendError(true);
|
||||||
this.sendError(true);
|
this.sendErrorDesc(
|
||||||
this.sendErrorDesc(e);
|
getNotification(iError, data?.ErrorMessage, Notifications.CantSendMessage)
|
||||||
this.sending(false);
|
+ "\n" + data?.ErrorMessageAdditional
|
||||||
|
);
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
this.sendError(false);
|
||||||
|
this.sending(true);
|
||||||
|
|
||||||
|
sSentFolder = UNUSED_OPTION_VALUE === sSentFolder ? '' : sSentFolder;
|
||||||
|
|
||||||
|
const sendMessage = params => {
|
||||||
|
Remote.request('SendMessage',
|
||||||
|
(iError, data) => {
|
||||||
|
this.sending(false);
|
||||||
|
if (iError) {
|
||||||
|
/*
|
||||||
|
if (Notifications.AuthError === iError && !params.auth) {
|
||||||
|
AskPopupView.password('SMTP login', 'retry', 3).then(result => {
|
||||||
|
if (result) {
|
||||||
|
this.sending(true);
|
||||||
|
params.auth = result;
|
||||||
|
sendMessage(params);
|
||||||
|
} else {
|
||||||
|
sendFailed(iError, data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else
|
||||||
|
*/
|
||||||
|
if (Notifications.CantSaveMessage === iError) {
|
||||||
|
this.sendSuccessButSaveError(true);
|
||||||
|
let msg = i18n('COMPOSE/SAVED_ERROR_ON_SEND');
|
||||||
|
if (data?.ErrorMessageAdditional) {
|
||||||
|
msg = msg + "\n" + data?.ErrorMessageAdditional;
|
||||||
|
}
|
||||||
|
this.savedErrorDesc(msg);
|
||||||
|
} else {
|
||||||
|
params.signPassphrase && Passphrases.delete(identity);
|
||||||
|
this.sendError(true);
|
||||||
|
sendFailed(iError, data);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (arrayLength(this.aDraftInfo) > 0) {
|
||||||
|
const flag = {
|
||||||
|
'reply': '\\answered',
|
||||||
|
'forward': '$forwarded'
|
||||||
|
}[this.aDraftInfo[0]];
|
||||||
|
if (flag) {
|
||||||
|
const aFlags = oLastMessage.flags();
|
||||||
|
if (aFlags.indexOf(flag) === -1) {
|
||||||
|
aFlags.push(flag);
|
||||||
|
oLastMessage.flags(aFlags);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
setFolderETag(this.draftsFolder(), '');
|
||||||
|
setFolderETag(sSentFolder, '');
|
||||||
|
if (3 === arrayLength(this.aDraftInfo)) {
|
||||||
|
const folder = this.aDraftInfo[2];
|
||||||
|
setFolderETag(folder, '');
|
||||||
|
}
|
||||||
|
reloadDraftFolder();
|
||||||
|
},
|
||||||
|
params,
|
||||||
|
30000
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
this.getMessageRequestParams(sSentFolder)
|
||||||
|
.then(sendMessage)
|
||||||
|
.catch(sendError);
|
||||||
|
} catch (e) {
|
||||||
|
sendError(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -700,7 +743,6 @@ export class ComposePopupView extends AbstractViewPopup {
|
||||||
// setTimeout(() => {
|
// setTimeout(() => {
|
||||||
this.oEditor = new HtmlEditor(
|
this.oEditor = new HtmlEditor(
|
||||||
this.editorArea(),
|
this.editorArea(),
|
||||||
null,
|
|
||||||
() => fOnInit(this.oEditor),
|
() => fOnInit(this.oEditor),
|
||||||
bHtml => this.isHtml(!!bHtml)
|
bHtml => this.isHtml(!!bHtml)
|
||||||
);
|
);
|
||||||
|
|
@ -853,7 +895,10 @@ export class ComposePopupView extends AbstractViewPopup {
|
||||||
|
|
||||||
switch (options.mode) {
|
switch (options.mode) {
|
||||||
case ComposeType.Reply:
|
case ComposeType.Reply:
|
||||||
case ComposeType.ReplyAll:
|
case ComposeType.ReplyAll: {
|
||||||
|
// if (1 == oLastMessage.to.length) {
|
||||||
|
// setTimeout(() => this.from(emailArrayToStringLineHelper(oLastMessage.to)), 1);
|
||||||
|
// }
|
||||||
if (ComposeType.Reply === options.mode) {
|
if (ComposeType.Reply === options.mode) {
|
||||||
this.to(emailArrayToStringLineHelper(oLastMessage.replyEmails(excludeEmail)));
|
this.to(emailArrayToStringLineHelper(oLastMessage.replyEmails(excludeEmail)));
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -866,9 +911,15 @@ export class ComposePopupView extends AbstractViewPopup {
|
||||||
this.aDraftInfo = ['reply', oLastMessage.uid, oLastMessage.folder];
|
this.aDraftInfo = ['reply', oLastMessage.uid, oLastMessage.folder];
|
||||||
this.sInReplyTo = oLastMessage.messageId;
|
this.sInReplyTo = oLastMessage.messageId;
|
||||||
this.sReferences = (oLastMessage.references + ' ' + oLastMessage.messageId).trim();
|
this.sReferences = (oLastMessage.references + ' ' + oLastMessage.messageId).trim();
|
||||||
// OpenPGP “Transferable Public Key”
|
oLastMessage.headers().valuesByName('autocrypt').forEach(value => {
|
||||||
// oLastMessage.autocrypt?.keydata
|
let autocrypt = new MimeHeaderAutocryptModel(value);
|
||||||
break;
|
if (autocrypt.addr && autocrypt.keydata) {
|
||||||
|
PgpUserStore.hasPublicKeyForEmails([autocrypt.addr])
|
||||||
|
|| PgpUserStore.importKey(autocrypt.pem(), true, true)
|
||||||
|
// || showScreenPopup(OpenPgpImportPopupView, [autocrypt.pem()])
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} break;
|
||||||
|
|
||||||
case ComposeType.Forward:
|
case ComposeType.Forward:
|
||||||
case ComposeType.ForwardAsAttachment:
|
case ComposeType.ForwardAsAttachment:
|
||||||
|
|
@ -1190,7 +1241,7 @@ export class ComposePopupView extends AbstractViewPopup {
|
||||||
const attachment = new ComposeAttachmentModel(
|
const attachment = new ComposeAttachmentModel(
|
||||||
message.requestHash,
|
message.requestHash,
|
||||||
message.subject() /*+ '-' + Jua.randomId()*/ + '.eml',
|
message.subject() /*+ '-' + Jua.randomId()*/ + '.eml',
|
||||||
message.size()
|
message.size
|
||||||
);
|
);
|
||||||
attachment.fromMessage = true;
|
attachment.fromMessage = true;
|
||||||
attachment.complete(true);
|
attachment.complete(true);
|
||||||
|
|
@ -1301,8 +1352,8 @@ export class ComposePopupView extends AbstractViewPopup {
|
||||||
this.showBcc(false);
|
this.showBcc(false);
|
||||||
this.showReplyTo(false);
|
this.showReplyTo(false);
|
||||||
|
|
||||||
this.pgpSign(SettingsUserStore.pgpSign());
|
this.doSign(SettingsUserStore.pgpSign());
|
||||||
this.pgpEncrypt(SettingsUserStore.pgpEncrypt());
|
this.doEncrypt(SettingsUserStore.pgpEncrypt());
|
||||||
|
|
||||||
this.attachments([]);
|
this.attachments([]);
|
||||||
|
|
||||||
|
|
@ -1338,28 +1389,55 @@ export class ComposePopupView extends AbstractViewPopup {
|
||||||
].join(',').split(',').map(value => getEmail(value.trim())).validUnique();
|
].join(',').split(',').map(value => getEmail(value.trim())).validUnique();
|
||||||
}
|
}
|
||||||
|
|
||||||
initPgpEncrypt() {
|
/**
|
||||||
const recipients = this.allRecipients();
|
* Checks if signing a message is possible with from email address.
|
||||||
PgpUserStore.hasPublicKeyForEmails(recipients).then(result => {
|
* And sets all that can.
|
||||||
console.log({canPgpEncrypt:result});
|
*/
|
||||||
this.canPgpEncrypt(result);
|
initSign() {
|
||||||
});
|
let options = [],
|
||||||
PgpUserStore.mailvelopeHasPublicKeyForEmails(recipients).then(result => {
|
identity = this.currentIdentity(),
|
||||||
console.log({canMailvelope:result});
|
email = getEmail(this.from()),
|
||||||
this.canMailvelope(result);
|
key = OpenPGPUserStore.getPrivateKeyFor(email, 1);
|
||||||
if (!result) {
|
key && options.push(['OpenPGP', key]);
|
||||||
|
key = GnuPGUserStore.getPrivateKeyFor(email, 1);
|
||||||
|
key && options.push(['GnuPG', key]);
|
||||||
|
identity.smimeKeyValid() && identity.smimeCertificateValid() && identity.email() === email
|
||||||
|
&& options.push(['S/MIME']);
|
||||||
|
console.dir({signOptions: options});
|
||||||
|
this.signOptions(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
async initEncrypt() {
|
||||||
|
const recipients = this.allRecipients(),
|
||||||
|
options = [];
|
||||||
|
|
||||||
|
if (recipients.length) {
|
||||||
|
GnuPGUserStore.hasPublicKeyForEmails(recipients)
|
||||||
|
&& options.push('GnuPG');
|
||||||
|
|
||||||
|
OpenPGPUserStore.hasPublicKeyForEmails(recipients)
|
||||||
|
&& options.push('OpenPGP');
|
||||||
|
|
||||||
|
const count = recipients.length,
|
||||||
|
identity = this.currentIdentity(),
|
||||||
|
from = (identity.smimeKey() && identity.smimeCertificate()) ? identity.email() : null;
|
||||||
|
count
|
||||||
|
&& count === recipients.filter(email =>
|
||||||
|
email == from
|
||||||
|
|| SMimeUserStore.find(certificate => email == certificate.emailAddress && certificate.smimeencrypt)
|
||||||
|
).length
|
||||||
|
&& options.push('S/MIME');
|
||||||
|
|
||||||
|
if (await MailvelopeUserStore.hasPublicKeyForEmails(recipients)) {
|
||||||
|
options.push('Mailvelope');
|
||||||
|
} else {
|
||||||
'mailvelope' === this.viewArea() && this.bodyArea();
|
'mailvelope' === this.viewArea() && this.bodyArea();
|
||||||
// this.dropMailvelope();
|
// this.dropMailvelope();
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
}
|
|
||||||
|
|
||||||
togglePgpSign() {
|
console.dir({encryptOptions:options});
|
||||||
this.pgpSign(!this.pgpSign()/* && this.canPgpSign()*/);
|
this.encryptOptions(options);
|
||||||
}
|
|
||||||
|
|
||||||
togglePgpEncrypt() {
|
|
||||||
this.pgpEncrypt(!this.pgpEncrypt()/* && this.canPgpEncrypt()*/);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getMessageRequestParams(sSaveFolder, draft)
|
async getMessageRequestParams(sSaveFolder, draft)
|
||||||
|
|
@ -1410,11 +1488,18 @@ export class ComposePopupView extends AbstractViewPopup {
|
||||||
// Only used at send, not at save:
|
// Only used at send, not at save:
|
||||||
dsn: this.requestDsn() ? 1 : 0,
|
dsn: this.requestDsn() ? 1 : 0,
|
||||||
requireTLS: this.requireTLS() ? 1 : 0,
|
requireTLS: this.requireTLS() ? 1 : 0,
|
||||||
readReceiptRequest: this.requestReadReceipt() ? 1 : 0
|
readReceiptRequest: this.requestReadReceipt() ? 1 : 0,
|
||||||
|
autocrypt: [],
|
||||||
|
/**
|
||||||
|
* Basic support for Linked Data (Structured Email)
|
||||||
|
* https://json-ld.org/
|
||||||
|
* https://structured.email/
|
||||||
|
**/
|
||||||
|
linkedData: []
|
||||||
},
|
},
|
||||||
recipients = draft ? [identity.email()] : this.allRecipients(),
|
recipients = draft ? [identity.email()] : this.allRecipients(),
|
||||||
sign = !draft && this.pgpSign() && this.canPgpSign(),
|
signOptions = !draft && this.doSign() && this.signOptions(),
|
||||||
encrypt = this.pgpEncrypt() && this.canPgpEncrypt(),
|
encryptOptions = this.doEncrypt() && this.encryptOptions(),
|
||||||
isHtml = this.oEditor.isHtml();
|
isHtml = this.oEditor.isHtml();
|
||||||
|
|
||||||
if (isHtml) {
|
if (isHtml) {
|
||||||
|
|
@ -1436,7 +1521,12 @@ export class ComposePopupView extends AbstractViewPopup {
|
||||||
params.encrypted = draft
|
params.encrypted = draft
|
||||||
? await this.mailvelope.createDraft()
|
? await this.mailvelope.createDraft()
|
||||||
: await this.mailvelope.encrypt(recipients);
|
: await this.mailvelope.encrypt(recipients);
|
||||||
} else if (sign || encrypt) {
|
/*
|
||||||
|
Object.entries(PgpUserStore.getPublicKeyOfEmails(recipients) || {}).forEach(([k,v]) =>
|
||||||
|
params.autocrypt.push({addr:k, keydata:v.replace(/-----(BEGIN|END) PGP PUBLIC KEY BLOCK-----/g).trim()})
|
||||||
|
);
|
||||||
|
*/
|
||||||
|
} else if (signOptions.length || encryptOptions.length) {
|
||||||
if (!draft && !hasAttachments && !Text.length) {
|
if (!draft && !hasAttachments && !Text.length) {
|
||||||
throw i18n('COMPOSE/ERROR_EMPTY_BODY');
|
throw i18n('COMPOSE/ERROR_EMPTY_BODY');
|
||||||
}
|
}
|
||||||
|
|
@ -1456,45 +1546,108 @@ export class ComposePopupView extends AbstractViewPopup {
|
||||||
alternative.children.push(data);
|
alternative.children.push(data);
|
||||||
data = alternative;
|
data = alternative;
|
||||||
}
|
}
|
||||||
if (!draft && sign?.[1]) {
|
let sign = true;
|
||||||
if ('openpgp' == sign[0]) {
|
for (let i = 0; i < signOptions.length; ++i) {
|
||||||
// Doesn't sign attachments
|
if ('OpenPGP' == signOptions[i][0]) {
|
||||||
params.html = params.plain = '';
|
try {
|
||||||
let signed = new MimePart;
|
// Doesn't sign attachments
|
||||||
signed.headers['Content-Type'] =
|
params.html = params.plain = '';
|
||||||
'multipart/signed; micalg="pgp-sha256"; protocol="application/pgp-signature"';
|
let signed = new MimePart;
|
||||||
signed.headers['Content-Transfer-Encoding'] = '7Bit';
|
signed.headers['Content-Type'] =
|
||||||
signed.children.push(data);
|
'multipart/signed; micalg="pgp-sha256"; protocol="application/pgp-signature"';
|
||||||
let signature = new MimePart;
|
signed.headers['Content-Transfer-Encoding'] = '7Bit';
|
||||||
signature.headers['Content-Type'] = 'application/pgp-signature; name="signature.asc"';
|
signed.children.push(data);
|
||||||
signature.headers['Content-Transfer-Encoding'] = '7Bit';
|
let signature = new MimePart;
|
||||||
signature.body = await OpenPGPUserStore.sign(data.toString(), sign[1], 1);
|
signature.headers['Content-Type'] = 'application/pgp-signature; name="signature.asc"';
|
||||||
signed.children.push(signature);
|
signature.headers['Content-Transfer-Encoding'] = '7Bit';
|
||||||
params.signed = signed.toString();
|
signature.body = await OpenPGPUserStore.sign(data.toString(), signOptions[i][1], 1);
|
||||||
params.boundary = signed.boundary;
|
signed.children.push(signature);
|
||||||
data = signed;
|
params.signed = signed.toString();
|
||||||
} else if ('gnupg' == sign[0]) {
|
params.boundary = signed.boundary;
|
||||||
|
data = signed;
|
||||||
|
/*
|
||||||
|
Object.entries(PgpUserStore.getPublicKeyOfEmails([getEmail(this.from())]) || {})
|
||||||
|
.forEach(([k,v]) => params.publicKey = v);
|
||||||
|
*/
|
||||||
|
} catch (e) {
|
||||||
|
sign = false;
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if ('GnuPG' == signOptions[i][0]) {
|
||||||
// TODO: sign in PHP fails
|
// TODO: sign in PHP fails
|
||||||
// params.signData = data.toString();
|
let pass = await GnuPGUserStore.sign(signOptions[i][1]);
|
||||||
params.signFingerprint = sign[1].fingerprint;
|
if (null != pass) {
|
||||||
params.signPassphrase = await GnuPGUserStore.sign(sign[1]);
|
// params.signData = data.toString();
|
||||||
} else {
|
params.signFingerprint = signOptions[i][1].fingerprint;
|
||||||
throw 'Signing with ' + sign[0] + ' not yet implemented';
|
params.signPassphrase = pass;
|
||||||
|
// params.attachPublicKey = false;
|
||||||
|
} else {
|
||||||
|
sign = false;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if ('S/MIME' == signOptions[i][0]) {
|
||||||
|
// TODO: sign in PHP fails
|
||||||
|
params.sign = 'S/MIME';
|
||||||
|
// params.signCertificate = identity.smimeCertificate();
|
||||||
|
// params.signPrivateKey = identity.smimeKey();
|
||||||
|
// params.attachCertificate = false;
|
||||||
|
if (identity.smimeKeyEncrypted()) {
|
||||||
|
const pass = await Passphrases.ask(identity,
|
||||||
|
i18n('SMIME/PRIVATE_KEY_OF', {EMAIL: identity.email()}),
|
||||||
|
'CRYPTO/SIGN'
|
||||||
|
);
|
||||||
|
if (null != pass) {
|
||||||
|
params.signPassphrase = pass.password;
|
||||||
|
// pass.remember && Passphrases.handle(identity, pass.password);
|
||||||
|
} else {
|
||||||
|
sign = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (encrypt) {
|
if (signOptions.length && !sign) {
|
||||||
if ('openpgp' == encrypt) {
|
throw 'Signing failed';
|
||||||
// Doesn't encrypt attachments
|
}
|
||||||
params.encrypted = await OpenPGPUserStore.encrypt(data.toString(), recipients);
|
|
||||||
params.signed = '';
|
if (encryptOptions.length) {
|
||||||
} else if ('gnupg' == encrypt) {
|
const autocrypt = () =>
|
||||||
// Does encrypt attachments
|
Object.entries(PgpUserStore.getPublicKeyOfEmails(recipients) || {}).forEach(([k,v]) =>
|
||||||
params.encryptFingerprints = JSON.stringify(GnuPGUserStore.getPublicKeyFingerprints(recipients));
|
params.autocrypt.push({
|
||||||
} else {
|
addr: k,
|
||||||
throw 'Encryption with ' + encrypt + ' not yet implemented';
|
keydata: v.replace(/-----(BEGIN|END) PGP PUBLIC KEY BLOCK-----/g, '').trim()
|
||||||
|
})
|
||||||
|
);
|
||||||
|
for (let i = 0; i < encryptOptions.length; ++i) {
|
||||||
|
if ('OpenPGP' == encryptOptions[i]) {
|
||||||
|
// Doesn't encrypt attachments
|
||||||
|
params.encrypted = await OpenPGPUserStore.encrypt(data.toString(), recipients);
|
||||||
|
params.signed = '';
|
||||||
|
autocrypt();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if ('GnuPG' == encryptOptions[i]) {
|
||||||
|
// Does encrypt attachments
|
||||||
|
params.encryptFingerprints = JSON.stringify(GnuPGUserStore.getPublicKeyFingerprints(recipients));
|
||||||
|
autocrypt();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if ('S/MIME' == encryptOptions[i]) {
|
||||||
|
params.encryptCertificates = [identity.smimeCertificate()];
|
||||||
|
SMimeUserStore.forEach(certificate => {
|
||||||
|
certificate.emailAddress != identity.email()
|
||||||
|
&& recipients.includes(certificate.emailAddress)
|
||||||
|
&& params.encryptCertificates.push(certificate.id)
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// We skip Mailvelope as it has its own window
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return params;
|
return params;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,7 @@ export class ContactsPopupView extends AbstractViewPopup {
|
||||||
|
|
||||||
this.selector.on('ItemSelect', contact => this.populateViewContact(contact));
|
this.selector.on('ItemSelect', contact => this.populateViewContact(contact));
|
||||||
|
|
||||||
this.selector.on('ItemGetUid', contact => contact ? contact.generateUid() : '');
|
this.selector.on('ItemGetUid', contact => contact ? contact.id() : '');
|
||||||
|
|
||||||
addComputablesTo(this, {
|
addComputablesTo(this, {
|
||||||
contactsPaginator: computedPaginatorHelper(
|
contactsPaginator: computedPaginatorHelper(
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ const
|
||||||
imapType: 0,
|
imapType: 0,
|
||||||
imapTimeout: 300,
|
imapTimeout: 300,
|
||||||
imapShortLogin: false,
|
imapShortLogin: false,
|
||||||
|
imapLowerLogin: true,
|
||||||
// SSL
|
// SSL
|
||||||
imapSslVerify_peer: false,
|
imapSslVerify_peer: false,
|
||||||
imapSslAllow_self_signed: false,
|
imapSslAllow_self_signed: false,
|
||||||
|
|
@ -44,12 +45,14 @@ const
|
||||||
sievePort: 4190,
|
sievePort: 4190,
|
||||||
sieveType: 0,
|
sieveType: 0,
|
||||||
sieveTimeout: 10,
|
sieveTimeout: 10,
|
||||||
|
sieveAuthLiteral: true,
|
||||||
|
|
||||||
smtpHost: '',
|
smtpHost: '',
|
||||||
smtpPort: 25,
|
smtpPort: 25,
|
||||||
smtpType: 0,
|
smtpType: 0,
|
||||||
smtpTimeout: 60,
|
smtpTimeout: 60,
|
||||||
smtpShortLogin: false,
|
smtpShortLogin: false,
|
||||||
|
smtpLowerLogin: true,
|
||||||
smtpUseAuth: true,
|
smtpUseAuth: true,
|
||||||
smtpSetSender: false,
|
smtpSetSender: false,
|
||||||
smtpAuthPlainLine: false,
|
smtpAuthPlainLine: false,
|
||||||
|
|
@ -69,6 +72,7 @@ const
|
||||||
secure: pInt(oDomain.imapType()),
|
secure: pInt(oDomain.imapType()),
|
||||||
timeout: oDomain.imapTimeout,
|
timeout: oDomain.imapTimeout,
|
||||||
shortLogin: !!oDomain.imapShortLogin(),
|
shortLogin: !!oDomain.imapShortLogin(),
|
||||||
|
lowerLogin: !!oDomain.imapLowerLogin(),
|
||||||
ssl: {
|
ssl: {
|
||||||
verify_peer: !!oDomain.imapSslVerify_peer(),
|
verify_peer: !!oDomain.imapSslVerify_peer(),
|
||||||
verify_peer_name: !!oDomain.imapSslVerify_peer(),
|
verify_peer_name: !!oDomain.imapSslVerify_peer(),
|
||||||
|
|
@ -92,6 +96,7 @@ const
|
||||||
secure: pInt(oDomain.smtpType()),
|
secure: pInt(oDomain.smtpType()),
|
||||||
timeout: oDomain.smtpTimeout,
|
timeout: oDomain.smtpTimeout,
|
||||||
shortLogin: !!oDomain.smtpShortLogin(),
|
shortLogin: !!oDomain.smtpShortLogin(),
|
||||||
|
lowerLogin: !!oDomain.smtpLowerLogin(),
|
||||||
ssl: {
|
ssl: {
|
||||||
verify_peer: !!oDomain.smtpSslVerify_peer(),
|
verify_peer: !!oDomain.smtpSslVerify_peer(),
|
||||||
verify_peer_name: !!oDomain.smtpSslVerify_peer(),
|
verify_peer_name: !!oDomain.smtpSslVerify_peer(),
|
||||||
|
|
@ -104,11 +109,13 @@ const
|
||||||
},
|
},
|
||||||
Sieve: {
|
Sieve: {
|
||||||
enabled: !!oDomain.sieveEnabled(),
|
enabled: !!oDomain.sieveEnabled(),
|
||||||
|
authLiteral: !!oDomain.sieveAuthLiteral(),
|
||||||
host: oDomain.sieveHost,
|
host: oDomain.sieveHost,
|
||||||
port: oDomain.sievePort,
|
port: oDomain.sievePort,
|
||||||
secure: pInt(oDomain.sieveType()),
|
secure: pInt(oDomain.sieveType()),
|
||||||
timeout: oDomain.sieveTimeout,
|
timeout: oDomain.sieveTimeout,
|
||||||
shortLogin: !!oDomain.imapShortLogin(),
|
shortLogin: !!oDomain.imapShortLogin(),
|
||||||
|
lowerLogin: !!oDomain.imapLowerLogin(),
|
||||||
ssl: {
|
ssl: {
|
||||||
verify_peer: !!oDomain.imapSslVerify_peer(),
|
verify_peer: !!oDomain.imapSslVerify_peer(),
|
||||||
verify_peer_name: !!oDomain.imapSslVerify_peer(),
|
verify_peer_name: !!oDomain.imapSslVerify_peer(),
|
||||||
|
|
@ -306,21 +313,43 @@ export class DomainPopupView extends AbstractViewPopup {
|
||||||
this.testingSmtpError(false);
|
this.testingSmtpError(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
autoconfig() {
|
||||||
|
let domain = this.name();
|
||||||
|
Remote.request('AdminDomainAutoconfig', (iError, oData) => {
|
||||||
|
if (oData?.Result?.config) {
|
||||||
|
let server = oData.Result.config.incomingServer[0];
|
||||||
|
this.imapHost(server.hostname);
|
||||||
|
this.imapPort(server.port);
|
||||||
|
this.imapType('STARTTLS' === server.socketType ? 2 : ('SSL' === server.socketType ? 1 : 0));
|
||||||
|
this.imapShortLogin('%EMAILADDRESS%' !== server.username);
|
||||||
|
|
||||||
|
server = oData.Result.config.outgoingServer[0];
|
||||||
|
this.smtpHost(server.hostname);
|
||||||
|
this.smtpPort(server.port);
|
||||||
|
this.smtpType('STARTTLS' === server.socketType ? 2 : ('SSL' === server.socketType ? 1 : 0));
|
||||||
|
this.smtpShortLogin('%EMAILADDRESS%' !== server.username);
|
||||||
|
this.smtpUseAuth(!!server.authentication);
|
||||||
|
this.smtpUsePhpMail(false);
|
||||||
|
}
|
||||||
|
}, {domain});
|
||||||
|
}
|
||||||
|
|
||||||
onShow(oDomain) {
|
onShow(oDomain) {
|
||||||
this.saving(false);
|
this.saving(false);
|
||||||
this.clearTesting();
|
this.clearTesting();
|
||||||
this.edit(false);
|
this.edit(false);
|
||||||
this.imapCapabilities([
|
this.imapCapabilities([
|
||||||
|
'BINARY',
|
||||||
'LIST-STATUS',
|
'LIST-STATUS',
|
||||||
'METADATA',
|
'METADATA',
|
||||||
'MOVE',
|
'MOVE',
|
||||||
|
'NAMESPACE',
|
||||||
|
'PREVIEW',
|
||||||
'SORT',
|
'SORT',
|
||||||
'THREAD',
|
|
||||||
'BINARY',
|
|
||||||
'STATUS=SIZE',
|
'STATUS=SIZE',
|
||||||
'PREVIEW'
|
'THREAD'
|
||||||
]);
|
]);
|
||||||
this.imapDisabled_capabilities(['PREVIEW','STATUS=SIZE']);
|
this.imapDisabled_capabilities(['METADATA','PREVIEW','STATUS=SIZE']);
|
||||||
forEachObjectEntry(domainDefaults, (key, value) => this[key](value));
|
forEachObjectEntry(domainDefaults, (key, value) => this[key](value));
|
||||||
this.enableSmartPorts(true);
|
this.enableSmartPorts(true);
|
||||||
if (oDomain) {
|
if (oDomain) {
|
||||||
|
|
@ -343,6 +372,8 @@ export class DomainPopupView extends AbstractViewPopup {
|
||||||
this[key]?.(value);
|
this[key]?.(value);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
this.name(IDN.toUnicode(this.name()));
|
||||||
|
this.aliasName(IDN.toUnicode(this.aliasName()));
|
||||||
this.imapCapabilities(this.imapCapabilities.concat(this.imapDisabled_capabilities()).unique());
|
this.imapCapabilities(this.imapCapabilities.concat(this.imapDisabled_capabilities()).unique());
|
||||||
this.enableSmartPorts(true);
|
this.enableSmartPorts(true);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
152
dev/View/Popup/Folder.js
Normal file
152
dev/View/Popup/Folder.js
Normal file
|
|
@ -0,0 +1,152 @@
|
||||||
|
import { AbstractViewPopup } from 'Knoin/AbstractViews';
|
||||||
|
import { addObservablesTo, koComputable } from 'External/ko';
|
||||||
|
import Remote from 'Remote/User/Fetch';
|
||||||
|
import { FolderUserStore } from 'Stores/User/Folder';
|
||||||
|
|
||||||
|
import { getFolderFromCacheList, setFolder, removeFolderFromCacheList } from 'Common/Cache';
|
||||||
|
import { Notifications } from 'Common/Enums';
|
||||||
|
import { FolderMetadataKeys } from 'Common/EnumsUser';
|
||||||
|
import { folderListOptionsBuilder, sortFolders } from 'Common/Folders';
|
||||||
|
import { initOnStartOrLangChange, i18n, getNotification } from 'Common/Translator';
|
||||||
|
import { defaultOptionsAfterRender } from 'Common/Utils';
|
||||||
|
|
||||||
|
export class FolderPopupView extends AbstractViewPopup {
|
||||||
|
constructor() {
|
||||||
|
super('Folder');
|
||||||
|
addObservablesTo(this, {
|
||||||
|
folder: null, // FolderModel
|
||||||
|
parentFolder: '',
|
||||||
|
name: '',
|
||||||
|
editing: false
|
||||||
|
});
|
||||||
|
this.ACLAllowed = FolderUserStore.hasCapability('ACL');
|
||||||
|
this.ACL = ko.observableArray();
|
||||||
|
|
||||||
|
this.parentFolderSelectList = koComputable(() =>
|
||||||
|
folderListOptionsBuilder(
|
||||||
|
[],
|
||||||
|
[['', '']],
|
||||||
|
oItem => oItem ? oItem.detailedName() : '',
|
||||||
|
item => !item.subFolders.allow
|
||||||
|
|| (FolderUserStore.namespace && !item.fullName.startsWith(FolderUserStore.namespace))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
this.displaySpecSetting = FolderUserStore.displaySpecSetting;
|
||||||
|
|
||||||
|
this.showKolab = FolderUserStore.allowKolab();
|
||||||
|
this.kolabTypeOptions = ko.observableArray();
|
||||||
|
let i18nFilter = key => i18n('SETTINGS_FOLDERS/TYPE_' + key);
|
||||||
|
initOnStartOrLangChange(()=>{
|
||||||
|
this.kolabTypeOptions([
|
||||||
|
{ id: '', name: '' },
|
||||||
|
{ id: 'event', name: i18nFilter('CALENDAR') },
|
||||||
|
{ id: 'contact', name: i18nFilter('CONTACTS') },
|
||||||
|
{ id: 'task', name: i18nFilter('TASKS') },
|
||||||
|
{ id: 'note', name: i18nFilter('NOTES') },
|
||||||
|
{ id: 'file', name: i18nFilter('FILES') },
|
||||||
|
{ id: 'journal', name: i18nFilter('JOURNAL') },
|
||||||
|
{ id: 'configuration', name: i18nFilter('CONFIGURATION') }
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.defaultOptionsAfterRender = defaultOptionsAfterRender;
|
||||||
|
}
|
||||||
|
|
||||||
|
afterHide() {
|
||||||
|
this.editing(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
submitForm(/*form*/) {
|
||||||
|
const
|
||||||
|
folder = this.folder(),
|
||||||
|
nameToEdit = this.name().trim(),
|
||||||
|
newParentName = this.parentFolder(),
|
||||||
|
oldParent = getFolderFromCacheList(folder.parentName),
|
||||||
|
newParent = getFolderFromCacheList(newParentName),
|
||||||
|
folderList = FolderUserStore.folderList,
|
||||||
|
newFolderList = newParent ? newParent.subFolders : folderList,
|
||||||
|
delimiter = (newParent || folder).delimiter,
|
||||||
|
oldFullname = folder.fullName,
|
||||||
|
newFullname = (newParent ? newParentName + delimiter : '') + nameToEdit;
|
||||||
|
if (nameToEdit && newFullname != oldFullname) {
|
||||||
|
Remote.abort('Folders').post('FolderRename', FolderUserStore.foldersRenaming, {
|
||||||
|
oldName: oldFullname,
|
||||||
|
newName: newFullname,
|
||||||
|
// toggleFolderSubscription / FolderSubscribe
|
||||||
|
subscribe: folder.isSubscribed() ? 1 : 0,
|
||||||
|
// toggleFolderCheckable / FolderCheckable
|
||||||
|
checkable: folder.checkable() ? 1 : 0,
|
||||||
|
// toggleFolderKolabType / FolderSetMetadata
|
||||||
|
kolab: {
|
||||||
|
// TODO: append '.default' ?
|
||||||
|
type: FolderMetadataKeys.KolabFolderType,
|
||||||
|
value: folder.kolabType()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
const
|
||||||
|
renameFolder = (folder, parent) => {
|
||||||
|
removeFolderFromCacheList(folder.fullName);
|
||||||
|
folder.parentName = (parent ? parent.fullName : '');
|
||||||
|
folder.fullName = (parent ? parent.fullName + delimiter : '') + folder.name();
|
||||||
|
folder.delimiter = delimiter;
|
||||||
|
folder.deep = (parent ? parent.deep : -1) + 1;
|
||||||
|
setFolder(folder);
|
||||||
|
},
|
||||||
|
renameChildren = folder => {
|
||||||
|
folder.subFolders.forEach(child => {
|
||||||
|
renameFolder(child, folder);
|
||||||
|
renameChildren(child);
|
||||||
|
})
|
||||||
|
};
|
||||||
|
folder.name(nameToEdit);
|
||||||
|
renameFolder(folder, newParent);
|
||||||
|
if (folder.subFolders.length || newParent != oldParent) {
|
||||||
|
// Rename all subfolders to prevent reload
|
||||||
|
renameChildren(folder);
|
||||||
|
}
|
||||||
|
(oldParent ? oldParent.subFolders : folderList).remove(folder);
|
||||||
|
newFolderList.push(folder);
|
||||||
|
sortFolders(newFolderList);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error(error);
|
||||||
|
FolderUserStore.error(
|
||||||
|
getNotification(error.code, '', Notifications.CantRenameFolder) + '.\n' + error.message
|
||||||
|
);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
Remote.request('FolderSettings', null, {
|
||||||
|
folder: folder.fullName,
|
||||||
|
// toggleFolderSubscription / FolderSubscribe
|
||||||
|
subscribe: folder.isSubscribed() ? 1 : 0,
|
||||||
|
// toggleFolderCheckable / FolderCheckable
|
||||||
|
checkable: folder.checkable() ? 1 : 0,
|
||||||
|
// toggleFolderKolabType / FolderSetMetadata
|
||||||
|
kolab: {
|
||||||
|
// TODO: append '.default' ?
|
||||||
|
type: FolderMetadataKeys.KolabFolderType,
|
||||||
|
value: folder.kolabType()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeShow(folder) {
|
||||||
|
this.ACL([]);
|
||||||
|
this.ACLAllowed && Remote.request('FolderACL', (iError, data) => {
|
||||||
|
if (!iError && data.Result) {
|
||||||
|
this.ACL(Object.values(data.Result));
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
folder: folder.fullName
|
||||||
|
});
|
||||||
|
this.editing(!folder.type() && folder.exists && folder.selectable());
|
||||||
|
this.name(folder.name()),
|
||||||
|
this.parentFolder(folder.parentName);
|
||||||
|
this.folder(folder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,7 +3,7 @@ import { koComputable, addObservablesTo } from 'External/ko';
|
||||||
import { Notifications } from 'Common/Enums';
|
import { Notifications } from 'Common/Enums';
|
||||||
import { defaultOptionsAfterRender } from 'Common/Utils';
|
import { defaultOptionsAfterRender } from 'Common/Utils';
|
||||||
import { folderListOptionsBuilder, sortFolders } from 'Common/Folders';
|
import { folderListOptionsBuilder, sortFolders } from 'Common/Folders';
|
||||||
import { getNotification } from 'Common/Translator';
|
import { getNotification/*, baseCollator*/ } from 'Common/Translator';
|
||||||
|
|
||||||
import { FolderUserStore } from 'Stores/User/Folder';
|
import { FolderUserStore } from 'Stores/User/Folder';
|
||||||
|
|
||||||
|
|
@ -56,7 +56,7 @@ export class FolderCreatePopupView extends AbstractViewPopup {
|
||||||
folders.push(subFolder);
|
folders.push(subFolder);
|
||||||
sortFolders(folders);
|
sortFolders(folders);
|
||||||
/*
|
/*
|
||||||
var collator = new Intl.Collator(undefined, {numeric: true, sensitivity: 'base'});
|
var collator = baseCollator(true);
|
||||||
console.log((folder ? folder.subFolders : FolderUserStore.folderList).sort(collator.compare));
|
console.log((folder ? folder.subFolders : FolderUserStore.folderList).sort(collator.compare));
|
||||||
*/
|
*/
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
|
|
||||||
import { addObservablesTo } from 'External/ko';
|
import { addObservablesTo, koComputable } from 'External/ko';
|
||||||
|
|
||||||
import { getNotification } from 'Common/Translator';
|
import { getNotification } from 'Common/Translator';
|
||||||
|
import { loadAccountsAndIdentities } from 'Common/UtilsUser';
|
||||||
|
|
||||||
import Remote from 'Remote/User/Fetch';
|
import Remote from 'Remote/User/Fetch';
|
||||||
|
|
||||||
|
|
@ -9,29 +10,21 @@ import { AbstractViewPopup } from 'Knoin/AbstractViews';
|
||||||
|
|
||||||
import { IdentityModel } from 'Model/Identity';
|
import { IdentityModel } from 'Model/Identity';
|
||||||
|
|
||||||
|
import { folderListOptionsBuilder } from 'Common/Folders';
|
||||||
|
import { i18n } from 'Common/Translator';
|
||||||
|
import { defaultOptionsAfterRender } from 'Common/Utils';
|
||||||
|
|
||||||
|
import { AskPopupView } from 'View/Popup/Ask';
|
||||||
|
|
||||||
export class IdentityPopupView extends AbstractViewPopup {
|
export class IdentityPopupView extends AbstractViewPopup {
|
||||||
constructor() {
|
constructor() {
|
||||||
super('Identity');
|
super('Identity');
|
||||||
|
|
||||||
addObservablesTo(this, {
|
addObservablesTo(this, {
|
||||||
id: '',
|
identity: null,
|
||||||
edit: false,
|
edit: false,
|
||||||
|
labelFocused: false,
|
||||||
email: '',
|
|
||||||
emailFocused: false,
|
|
||||||
|
|
||||||
name: '',
|
|
||||||
nameFocused: false,
|
nameFocused: false,
|
||||||
|
|
||||||
replyTo: '',
|
|
||||||
showReplyTo: false,
|
|
||||||
|
|
||||||
bcc: '',
|
|
||||||
showBcc: false,
|
|
||||||
|
|
||||||
signature: '',
|
|
||||||
signatureInsertBefore: false,
|
|
||||||
|
|
||||||
submitRequest: false,
|
submitRequest: false,
|
||||||
submitError: ''
|
submitError: ''
|
||||||
});
|
});
|
||||||
|
|
@ -40,21 +33,79 @@ export class IdentityPopupView extends AbstractViewPopup {
|
||||||
this.replyTo.valueHasMutated();
|
this.replyTo.valueHasMutated();
|
||||||
this.bcc.valueHasMutated();
|
this.bcc.valueHasMutated();
|
||||||
*/
|
*/
|
||||||
|
this.folderSelectList = koComputable(() =>
|
||||||
|
folderListOptionsBuilder(
|
||||||
|
[],
|
||||||
|
[['', '('+i18n('GLOBAL/DEFAULT')+')']]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
this.defaultOptionsAfterRender = defaultOptionsAfterRender;
|
||||||
|
|
||||||
|
this.createSelfSigned = this.createSelfSigned.bind(this);
|
||||||
|
this.setSMimeKeyPass = this.setSMimeKeyPass.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
createSelfSigned() {
|
||||||
|
AskPopupView.password('', 'CRYPTO/CREATE_SELF_SIGNED').then(result => {
|
||||||
|
if (result) {
|
||||||
|
const identity = this.identity();
|
||||||
|
Remote.request('SMimeCreateCertificate', (iError, oData) => {
|
||||||
|
if (oData.Result.x509) {
|
||||||
|
identity.smimeKey(oData.Result.pkey);
|
||||||
|
identity.smimeCertificate(oData.Result.x509);
|
||||||
|
} else {
|
||||||
|
this.submitError(oData.ErrorMessage);
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
name: identity.name(),
|
||||||
|
email: identity.email(),
|
||||||
|
privateKey: identity.smimeKey(),
|
||||||
|
passphrase: result.password
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async setSMimeKeyPass() {
|
||||||
|
const identity = this.identity();
|
||||||
|
let old = null
|
||||||
|
if (identity.smimeKeyEncrypted()) {
|
||||||
|
old = await AskPopupView.password(i18n('CRYPTO/CURRENT_PASS'), 'CRYPTO/DECRYPT');
|
||||||
|
if (!old) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AskPopupView.password(i18n('CRYPTO/NEW_PASS'), 'GLOBAL/SAVE').then(result => {
|
||||||
|
if (result) {
|
||||||
|
Remote.request('SMimeExportPrivateKey', (iError, oData) => {
|
||||||
|
if (oData.Result) {
|
||||||
|
identity.smimeKey(oData.Result);
|
||||||
|
} else {
|
||||||
|
this.submitError(oData.ErrorMessage);
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
privateKey: identity.smimeKey(),
|
||||||
|
oldPassphrase: old?.password,
|
||||||
|
newPassphrase: result.password
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
submitForm(form) {
|
submitForm(form) {
|
||||||
if (!this.submitRequest() && form.reportValidity()) {
|
if (!this.submitRequest() && form.reportValidity()) {
|
||||||
this.signature?.__fetchEditorValue?.();
|
let identity = this.identity();
|
||||||
|
identity.signature?.__fetchEditorValue?.();
|
||||||
this.submitRequest(true);
|
this.submitRequest(true);
|
||||||
const data = new FormData(form);
|
const data = new FormData(form);
|
||||||
data.set('Id', this.id());
|
data.set('Id', identity.id());
|
||||||
data.set('Signature', this.signature());
|
data.set('Signature', identity.signature());
|
||||||
Remote.request('IdentityUpdate', iError => {
|
Remote.request('IdentityUpdate', iError => {
|
||||||
this.submitRequest(false);
|
this.submitRequest(false);
|
||||||
if (iError) {
|
if (iError) {
|
||||||
this.submitError(getNotification(iError));
|
this.submitError(getNotification(iError));
|
||||||
} else {
|
} else {
|
||||||
rl.app.accountsAndIdentities();
|
loadAccountsAndIdentities();
|
||||||
this.close();
|
this.close();
|
||||||
}
|
}
|
||||||
}, data
|
}, data
|
||||||
|
|
@ -66,12 +117,8 @@ export class IdentityPopupView extends AbstractViewPopup {
|
||||||
* @param {?IdentityModel} oIdentity
|
* @param {?IdentityModel} oIdentity
|
||||||
*/
|
*/
|
||||||
onShow(identity) {
|
onShow(identity) {
|
||||||
this.showBcc(false);
|
|
||||||
this.showReplyTo(false);
|
|
||||||
|
|
||||||
this.submitRequest(false);
|
this.submitRequest(false);
|
||||||
this.submitError('');
|
this.submitError('');
|
||||||
|
|
||||||
if (identity) {
|
if (identity) {
|
||||||
this.edit(true);
|
this.edit(true);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -79,18 +126,10 @@ export class IdentityPopupView extends AbstractViewPopup {
|
||||||
identity = new IdentityModel;
|
identity = new IdentityModel;
|
||||||
identity.id(Jua.randomId());
|
identity.id(Jua.randomId());
|
||||||
}
|
}
|
||||||
this.id(identity.id() || '');
|
this.identity(identity);
|
||||||
this.name(identity.name());
|
|
||||||
this.email(identity.email());
|
|
||||||
this.replyTo(identity.replyTo());
|
|
||||||
this.showReplyTo(0 < identity.replyTo().length);
|
|
||||||
this.bcc(identity.bcc());
|
|
||||||
this.showBcc(0 < identity.bcc().length);
|
|
||||||
this.signature(identity.signature());
|
|
||||||
this.signatureInsertBefore(identity.signatureInsertBefore());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
afterShow() {
|
afterShow() {
|
||||||
this.id() ? this.emailFocused(true) : this.nameFocused(true);
|
this.identity().id() ? this.labelFocused(true) : this.nameFocused(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ export class OpenPgpGeneratePopupView extends AbstractViewPopup {
|
||||||
const type = this.keyType().toLowerCase(),
|
const type = this.keyType().toLowerCase(),
|
||||||
userId = {
|
userId = {
|
||||||
name: this.name(),
|
name: this.name(),
|
||||||
email: this.email()
|
email: IDN.toASCII(this.email())
|
||||||
},
|
},
|
||||||
cfg = {
|
cfg = {
|
||||||
type: type,
|
type: type,
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,25 @@
|
||||||
import { addObservablesTo } from 'External/ko';
|
import { addObservablesTo } from 'External/ko';
|
||||||
import { GnuPGUserStore } from 'Stores/User/GnuPG';
|
import { GnuPGUserStore } from 'Stores/User/GnuPG';
|
||||||
import { OpenPGPUserStore } from 'Stores/User/OpenPGP';
|
import { PgpUserStore } from 'Stores/User/Pgp';
|
||||||
|
|
||||||
import { AbstractViewPopup } from 'Knoin/AbstractViews';
|
import { AbstractViewPopup } from 'Knoin/AbstractViews';
|
||||||
|
|
||||||
|
import Remote from 'Remote/User/Fetch';
|
||||||
|
import { i18n } from 'Common/Translator';
|
||||||
|
|
||||||
export class OpenPgpImportPopupView extends AbstractViewPopup {
|
export class OpenPgpImportPopupView extends AbstractViewPopup {
|
||||||
constructor() {
|
constructor() {
|
||||||
super('OpenPgpImport');
|
super('OpenPgpImport');
|
||||||
|
|
||||||
addObservablesTo(this, {
|
addObservablesTo(this, {
|
||||||
|
search: '',
|
||||||
|
|
||||||
key: '',
|
key: '',
|
||||||
keyError: false,
|
keyError: false,
|
||||||
keyErrorMessage: '',
|
keyErrorMessage: '',
|
||||||
|
|
||||||
saveGnuPG: true,
|
saveGnuPG: true,
|
||||||
saveServer: false
|
saveServer: true
|
||||||
});
|
});
|
||||||
|
|
||||||
this.canGnuPG = GnuPGUserStore.isSupported();
|
this.canGnuPG = GnuPGUserStore.isSupported();
|
||||||
|
|
@ -25,6 +30,44 @@ export class OpenPgpImportPopupView extends AbstractViewPopup {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
searchPGP() {
|
||||||
|
this.key(i18n('SUGGESTIONS/SEARCHING_DESC'));
|
||||||
|
const fn = () => Remote.request('PgpSearchKey',
|
||||||
|
(iError, oData) => {
|
||||||
|
if (iError) {
|
||||||
|
this.key(oData.ErrorMessage);
|
||||||
|
} else {
|
||||||
|
this.key(oData.Result);
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
query: this.search()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
fetch(
|
||||||
|
`https://keys.openpgp.org/pks/lookup?op=get&options=mr&search=${this.search()}`,
|
||||||
|
{
|
||||||
|
method: 'GET',
|
||||||
|
mode: 'cors',
|
||||||
|
cache: 'no-cache',
|
||||||
|
redirect: 'error',
|
||||||
|
referrerPolicy: 'no-referrer',
|
||||||
|
credentials: 'omit'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.then(response => {
|
||||||
|
if ('application/pgp-keys' == response.headers.get('Content-Type')) {
|
||||||
|
response.text().then(body => this.key(body));
|
||||||
|
} else {
|
||||||
|
fn();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(e => {
|
||||||
|
this.key('keys.openpgp.org: ' + e?.message + '\nTrying local...');
|
||||||
|
fn();
|
||||||
|
throw e;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
submitForm() {
|
submitForm() {
|
||||||
let keyTrimmed = this.key().trim();
|
let keyTrimmed = this.key().trim();
|
||||||
|
|
||||||
|
|
@ -35,38 +78,30 @@ export class OpenPgpImportPopupView extends AbstractViewPopup {
|
||||||
this.keyError(!keyTrimmed);
|
this.keyError(!keyTrimmed);
|
||||||
this.keyErrorMessage('');
|
this.keyErrorMessage('');
|
||||||
|
|
||||||
if (!keyTrimmed) {
|
if (keyTrimmed) {
|
||||||
return;
|
let match = null,
|
||||||
}
|
count = 30,
|
||||||
|
|
||||||
let match = null,
|
|
||||||
count = 30,
|
|
||||||
done = false;
|
|
||||||
// eslint-disable-next-line max-len
|
|
||||||
const reg = /[-]{3,6}BEGIN[\s]PGP[\s](PRIVATE|PUBLIC)[\s]KEY[\s]BLOCK[-]{3,6}[\s\S]+?[-]{3,6}END[\s]PGP[\s](PRIVATE|PUBLIC)[\s]KEY[\s]BLOCK[-]{3,6}/gi;
|
|
||||||
|
|
||||||
do {
|
|
||||||
match = reg.exec(keyTrimmed);
|
|
||||||
if (match && 0 < count) {
|
|
||||||
if (match[0] && match[1] && match[2] && match[1] === match[2]) {
|
|
||||||
this.saveGnuPG() && GnuPGUserStore.isSupported() && GnuPGUserStore.importKey(this.key(), (iError, oData) => {
|
|
||||||
iError && alert(oData.ErrorMessage);
|
|
||||||
});
|
|
||||||
OpenPGPUserStore.isSupported() && OpenPGPUserStore.importKey(this.key());
|
|
||||||
}
|
|
||||||
|
|
||||||
--count;
|
|
||||||
done = false;
|
done = false;
|
||||||
} else {
|
const GnuPG = this.saveGnuPG() && GnuPGUserStore.isSupported(),
|
||||||
done = true;
|
backup = this.saveServer(),
|
||||||
}
|
// eslint-disable-next-line max-len
|
||||||
} while (!done);
|
reg = /[-]{3,6}BEGIN[\s]PGP[\s](PRIVATE|PUBLIC)[\s]KEY[\s]BLOCK[-]{3,6}[\s\S]+?[-]{3,6}END[\s]PGP[\s](PRIVATE|PUBLIC)[\s]KEY[\s]BLOCK[-]{3,6}/gi;
|
||||||
|
|
||||||
if (this.keyError()) {
|
do {
|
||||||
return;
|
match = reg.exec(keyTrimmed);
|
||||||
|
if (match && 0 < count) {
|
||||||
|
if (match[0] && match[1] && match[2] && match[1] === match[2]) {
|
||||||
|
PgpUserStore.importKey(this.key(), GnuPG, backup);
|
||||||
|
}
|
||||||
|
--count;
|
||||||
|
done = false;
|
||||||
|
} else {
|
||||||
|
done = true;
|
||||||
|
}
|
||||||
|
} while (!done);
|
||||||
|
|
||||||
|
this.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.close();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onShow(key) {
|
onShow(key) {
|
||||||
|
|
|
||||||
50
dev/View/Popup/SMimeImport.js
Normal file
50
dev/View/Popup/SMimeImport.js
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
import { addObservablesTo } from 'External/ko';
|
||||||
|
import { getNotification } from 'Common/Translator';
|
||||||
|
|
||||||
|
import { AbstractViewPopup } from 'Knoin/AbstractViews';
|
||||||
|
|
||||||
|
import Remote from 'Remote/User/Fetch';
|
||||||
|
|
||||||
|
export class SMimeImportPopupView extends AbstractViewPopup {
|
||||||
|
constructor() {
|
||||||
|
super('SMimeImport');
|
||||||
|
|
||||||
|
addObservablesTo(this, {
|
||||||
|
pem: '',
|
||||||
|
pemError: false,
|
||||||
|
pemErrorMessage: '',
|
||||||
|
pemValid: false
|
||||||
|
});
|
||||||
|
|
||||||
|
this.pem.subscribe(value => {
|
||||||
|
this.pemError(false);
|
||||||
|
this.pemErrorMessage('');
|
||||||
|
this.pemValid(value && value.includes('-----BEGIN CERTIFICATE-----'));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
submitForm() {
|
||||||
|
if (this.pemValid()) {
|
||||||
|
Remote.request('SMimeImportCertificate',
|
||||||
|
(iError, oData) => {
|
||||||
|
if (iError) {
|
||||||
|
this.pemError(true);
|
||||||
|
this.pemErrorMessage(getNotification(iError, oData?.ErrorMessage));
|
||||||
|
// oData?.ErrorMessageAdditional;
|
||||||
|
} else {
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{pem:this.pem()}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.pemError(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onShow() {
|
||||||
|
this.pem('');
|
||||||
|
this.pemError(false);
|
||||||
|
this.pemErrorMessage('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -92,10 +92,6 @@ export class LoginUserView extends AbstractViewLogin {
|
||||||
this.submitError('');
|
this.submitError('');
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleSignMe() {
|
|
||||||
this.signMe(!this.signMe());
|
|
||||||
}
|
|
||||||
|
|
||||||
submitCommand(self, event) {
|
submitCommand(self, event) {
|
||||||
const email = this.email().trim();
|
const email = this.email().trim();
|
||||||
this.email(email);
|
this.email(email);
|
||||||
|
|
@ -124,7 +120,7 @@ export class LoginUserView extends AbstractViewLogin {
|
||||||
iError = Notifications.AuthError;
|
iError = Notifications.AuthError;
|
||||||
}
|
}
|
||||||
this.submitError(getNotification(iError, oData?.ErrorMessage,
|
this.submitError(getNotification(iError, oData?.ErrorMessage,
|
||||||
Notifications.UnknownNotification));
|
Notifications.UnknownError));
|
||||||
this.submitErrorAdditional(oData?.ErrorMessageAdditional);
|
this.submitErrorAdditional(oData?.ErrorMessageAdditional);
|
||||||
} else {
|
} else {
|
||||||
rl.setData(oData.Result);
|
rl.setData(oData.Result);
|
||||||
|
|
@ -142,25 +138,20 @@ export class LoginUserView extends AbstractViewLogin {
|
||||||
onBuild(dom) {
|
onBuild(dom) {
|
||||||
super.onBuild(dom);
|
super.onBuild(dom);
|
||||||
|
|
||||||
const signMe = (SettingsGet('signMe') || '').toLowerCase();
|
let signMe = SettingsGet('signMe');
|
||||||
|
|
||||||
switch (signMe) {
|
switch (signMe) {
|
||||||
case 'defaultoff':
|
case SignMeOff:
|
||||||
case 'defaulton':
|
case SignMeOn:
|
||||||
this.signMeType(
|
|
||||||
'defaulton' === signMe ? SignMeOn : SignMeOff
|
|
||||||
);
|
|
||||||
|
|
||||||
switch (Local.get(ClientSideKeyNameLastSignMe)) {
|
switch (Local.get(ClientSideKeyNameLastSignMe)) {
|
||||||
case '-1-':
|
case '-1-':
|
||||||
this.signMeType(SignMeOn);
|
signMe = SignMeOn;
|
||||||
break;
|
break;
|
||||||
case '-0-':
|
case '-0-':
|
||||||
this.signMeType(SignMeOff);
|
signMe = SignMeOff;
|
||||||
break;
|
break;
|
||||||
// no default
|
// no default
|
||||||
}
|
}
|
||||||
|
this.signMeType(signMe);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
this.signMeType(SignMeUnused);
|
this.signMeType(SignMeUnused);
|
||||||
|
|
|
||||||
|
|
@ -35,31 +35,18 @@ export class MailFolderList extends AbstractViewLeft {
|
||||||
|
|
||||||
this.moveAction = moveAction;
|
this.moveAction = moveAction;
|
||||||
|
|
||||||
this.foldersListWithSingleInboxRootFolder = ko.observable(false);
|
|
||||||
|
|
||||||
this.allowContacts = AppUserStore.allowContacts();
|
this.allowContacts = AppUserStore.allowContacts();
|
||||||
|
|
||||||
this.foldersFilter = foldersFilter;
|
this.foldersFilter = foldersFilter;
|
||||||
|
|
||||||
|
this.filterUnseen = ko.observable(false);
|
||||||
|
|
||||||
addComputablesTo(this, {
|
addComputablesTo(this, {
|
||||||
foldersFilterVisible: () => 20 < FolderUserStore.folderList().CountRec,
|
foldersFilterVisible: () => 20 < FolderUserStore.folderList().CountRec,
|
||||||
|
|
||||||
folderListVisible: () => {
|
folderListVisible: () => {
|
||||||
let multiple = false,
|
let result = FolderUserStore.folderList().visible();
|
||||||
inbox, visible,
|
return 1 === result.length && result[0].isInbox() ? result[0].visibleSubfolders() : result;
|
||||||
result = FolderUserStore.folderList().filter(folder => {
|
|
||||||
if (folder.isInbox()) {
|
|
||||||
inbox = folder;
|
|
||||||
}
|
|
||||||
visible = folder.visible();
|
|
||||||
multiple |= visible && !folder.isInbox();
|
|
||||||
return visible;
|
|
||||||
});
|
|
||||||
if (inbox && !multiple) {
|
|
||||||
inbox.collapsed(false);
|
|
||||||
}
|
|
||||||
this.foldersListWithSingleInboxRootFolder(!multiple);
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -90,13 +77,14 @@ export class MailFolderList extends AbstractViewLeft {
|
||||||
const folder = ko.dataFor(el);
|
const folder = ko.dataFor(el);
|
||||||
if (folder) {
|
if (folder) {
|
||||||
if (moveAction()) {
|
if (moveAction()) {
|
||||||
moveAction(false);
|
const copy = event.ctrlKey || 2 === moveAction(),
|
||||||
let messages = MessagelistUserStore.listCheckedOrSelectedUidsWithSubMails();
|
messages = MessagelistUserStore.listCheckedOrSelectedUidsWithSubMails();
|
||||||
|
moveAction(0);
|
||||||
messages.size && MessagelistUserStore.moveMessages(
|
messages.size && MessagelistUserStore.moveMessages(
|
||||||
messages.folder,
|
messages.folder,
|
||||||
messages,
|
messages,
|
||||||
folder.fullName,
|
folder.fullName,
|
||||||
event.ctrlKey
|
copy
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
if (!SettingsUserStore.usePreviewPane()) {
|
if (!SettingsUserStore.usePreviewPane()) {
|
||||||
|
|
@ -173,7 +161,7 @@ export class MailFolderList extends AbstractViewLeft {
|
||||||
// addShortcut('tab', 'shift', ScopeFolderList, () => {
|
// addShortcut('tab', 'shift', ScopeFolderList, () => {
|
||||||
addShortcut('escape,tab,arrowright', '', ScopeFolderList, () => {
|
addShortcut('escape,tab,arrowright', '', ScopeFolderList, () => {
|
||||||
AppUserStore.focusedState(ScopeMessageList);
|
AppUserStore.focusedState(ScopeMessageList);
|
||||||
moveAction(false);
|
moveAction(0);
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -162,11 +162,11 @@ export class MailMessageList extends AbstractViewRight {
|
||||||
let list = [], current, sort = FolderUserStore.sortMode() || 'DATE';
|
let list = [], current, sort = FolderUserStore.sortMode() || 'DATE';
|
||||||
if (sort.includes('FROM')) {
|
if (sort.includes('FROM')) {
|
||||||
MessagelistUserStore.forEach(msg => {
|
MessagelistUserStore.forEach(msg => {
|
||||||
let email = msg.from[0].email;
|
let email = msg.from[0]?.email;
|
||||||
if (!current || email != current.id) {
|
if (!current || email != current.id) {
|
||||||
current = {
|
current = {
|
||||||
id: email,
|
id: email,
|
||||||
label: msg.from[0].toLine(),
|
label: msg.from[0]?.toLine(),
|
||||||
search: 'from=' + email,
|
search: 'from=' + email,
|
||||||
messages: []
|
messages: []
|
||||||
};
|
};
|
||||||
|
|
@ -243,7 +243,7 @@ export class MailMessageList extends AbstractViewRight {
|
||||||
|
|
||||||
this.selector.on('MiddleClick', message => populateMessageBody(message, true));
|
this.selector.on('MiddleClick', message => populateMessageBody(message, true));
|
||||||
|
|
||||||
this.selector.on('ItemGetUid', message => (message ? message.generateUid() : ''));
|
this.selector.on('ItemGetUid', message => (message ? message.folder + '/' + message.uid : ''));
|
||||||
|
|
||||||
this.selector.on('canSelect', () => MessagelistUserStore.canSelect());
|
this.selector.on('canSelect', () => MessagelistUserStore.canSelect());
|
||||||
|
|
||||||
|
|
@ -331,6 +331,7 @@ export class MailMessageList extends AbstractViewRight {
|
||||||
spamCommand: canBeMovedHelper,
|
spamCommand: canBeMovedHelper,
|
||||||
notSpamCommand: canBeMovedHelper,
|
notSpamCommand: canBeMovedHelper,
|
||||||
moveCommand: canBeMovedHelper,
|
moveCommand: canBeMovedHelper,
|
||||||
|
copyCommand: canBeMovedHelper
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -415,18 +416,26 @@ export class MailMessageList extends AbstractViewRight {
|
||||||
moveMessagesToFolderType(FolderType.Inbox);
|
moveMessagesToFolderType(FolderType.Inbox);
|
||||||
}
|
}
|
||||||
|
|
||||||
moveCommand(vm, event) {
|
moveOrCopy(vm, event, mode) {
|
||||||
if (canBeMovedHelper()) {
|
if (canBeMovedHelper()) {
|
||||||
if (vm && event?.preventDefault) {
|
if (vm && event?.preventDefault) {
|
||||||
stopEvent(event);
|
stopEvent(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
let b = moveAction();
|
let i = moveAction();
|
||||||
AppUserStore.focusedState(b ? ScopeMessageList : ScopeFolderList);
|
AppUserStore.focusedState(i ? ScopeMessageList : ScopeFolderList);
|
||||||
moveAction(!b);
|
moveAction(i ? 0 : mode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
moveCommand(vm, event) {
|
||||||
|
this.moveOrCopy(vm, event, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
copyCommand(vm, event) {
|
||||||
|
this.moveOrCopy(vm, event, 2);
|
||||||
|
}
|
||||||
|
|
||||||
composeClick() {
|
composeClick() {
|
||||||
showMessageComposer();
|
showMessageComposer();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,9 @@ import { showScreenPopup } from 'Knoin/Knoin';
|
||||||
import { OpenPgpImportPopupView } from 'View/Popup/OpenPgpImport';
|
import { OpenPgpImportPopupView } from 'View/Popup/OpenPgpImport';
|
||||||
import { GnuPGUserStore } from 'Stores/User/GnuPG';
|
import { GnuPGUserStore } from 'Stores/User/GnuPG';
|
||||||
import { OpenPGPUserStore } from 'Stores/User/OpenPGP';
|
import { OpenPGPUserStore } from 'Stores/User/OpenPGP';
|
||||||
|
import { IdentityUserStore } from 'Stores/User/Identity';
|
||||||
|
|
||||||
|
import { Passphrases } from 'Storage/Passphrases';
|
||||||
|
|
||||||
const
|
const
|
||||||
oMessageScrollerDom = () => elementById('messageItem') || {},
|
oMessageScrollerDom = () => elementById('messageItem') || {},
|
||||||
|
|
@ -109,7 +112,8 @@ export class MailMessageView extends AbstractViewRight {
|
||||||
actionsMenu: null,
|
actionsMenu: null,
|
||||||
// viewer
|
// viewer
|
||||||
viewFromShort: '',
|
viewFromShort: '',
|
||||||
dkimData: ['none', '', '']
|
dkimData: ['none', '', ''],
|
||||||
|
nowTracking: false
|
||||||
});
|
});
|
||||||
|
|
||||||
this.moveAction = moveAction;
|
this.moveAction = moveAction;
|
||||||
|
|
@ -142,7 +146,7 @@ export class MailMessageView extends AbstractViewRight {
|
||||||
|
|
||||||
downloadAsZipAllowed: () => this.attachmentsActions.includes('zip')
|
downloadAsZipAllowed: () => this.attachmentsActions.includes('zip')
|
||||||
&& (currentMessage()?.attachments || [])
|
&& (currentMessage()?.attachments || [])
|
||||||
.filter(item => item?.download /*&& !item?.isLinked()*/ && item?.checked())
|
.filter(item => item?.checked() && item?.download /*&& !item?.isLinked()*/)
|
||||||
.length,
|
.length,
|
||||||
|
|
||||||
tagsAllowed: () => FolderUserStore.currentFolder()?.tagsAllowed(),
|
tagsAllowed: () => FolderUserStore.currentFolder()?.tagsAllowed(),
|
||||||
|
|
@ -155,10 +159,10 @@ export class MailMessageView extends AbstractViewRight {
|
||||||
: ''
|
: ''
|
||||||
).join(' '),
|
).join(' '),
|
||||||
|
|
||||||
askReadReceipt: () =>
|
askReadReceipt: () => currentMessage()?.readReceipt
|
||||||
(MessagelistUserStore.isDraftFolder() || MessagelistUserStore.isSentFolder())
|
&& !(MessagelistUserStore.isDraftFolder() || MessagelistUserStore.isSentFolder())
|
||||||
&& currentMessage()?.readReceipt()
|
&& !currentMessage()?.flags().includes('$mdnsent')
|
||||||
&& currentMessage()?.flags().includes('$mdnsent'),
|
&& !currentMessage()?.flags().includes('\\answered'),
|
||||||
|
|
||||||
listAttachments: () => currentMessage()?.attachments()
|
listAttachments: () => currentMessage()?.attachments()
|
||||||
.filter(item => SettingsUserStore.listInlineAttachments() || !item.isLinked()),
|
.filter(item => SettingsUserStore.listInlineAttachments() || !item.isLinked()),
|
||||||
|
|
@ -170,18 +174,17 @@ export class MailMessageView extends AbstractViewRight {
|
||||||
|
|
||||||
canBeRepliedOrForwarded: () => !MessagelistUserStore.isDraftFolder() && this.messageVisible(),
|
canBeRepliedOrForwarded: () => !MessagelistUserStore.isDraftFolder() && this.messageVisible(),
|
||||||
|
|
||||||
viewDkimIcon: () => 'none' !== this.dkimData()[0],
|
dkimIcon: () => {
|
||||||
|
|
||||||
dkimIconClass:() => {
|
|
||||||
switch (this.dkimData()[0]) {
|
switch (this.dkimData()[0]) {
|
||||||
case 'none':
|
case 'none':
|
||||||
return '';
|
return '';
|
||||||
case 'pass':
|
case 'pass':
|
||||||
return 'icon-ok iconcolor-green'; // ✔️
|
return '✔';
|
||||||
default:
|
default:
|
||||||
return 'icon-cross iconcolor-red'; // ✖ ❌
|
return '✖';
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
dkimIconClass: () => 'pass' === this.dkimData()[0] ? 'iconcolor-green' : 'iconcolor-red',
|
||||||
|
|
||||||
dkimTitle:() => {
|
dkimTitle:() => {
|
||||||
const dkim = this.dkimData();
|
const dkim = this.dkimData();
|
||||||
|
|
@ -208,6 +211,7 @@ export class MailMessageView extends AbstractViewRight {
|
||||||
// TODO: make first param a user setting #683
|
// TODO: make first param a user setting #683
|
||||||
this.viewFromShort(message.from.toString(false, true));
|
this.viewFromShort(message.from.toString(false, true));
|
||||||
this.dkimData(message.dkim[0] || ['none', '', '']);
|
this.dkimData(message.dkim[0] || ['none', '', '']);
|
||||||
|
this.nowTracking(false);
|
||||||
} else {
|
} else {
|
||||||
MessagelistUserStore.selectedMessage(null);
|
MessagelistUserStore.selectedMessage(null);
|
||||||
|
|
||||||
|
|
@ -236,6 +240,7 @@ export class MailMessageView extends AbstractViewRight {
|
||||||
decorateKoCommands(this, {
|
decorateKoCommands(this, {
|
||||||
editCommand: self => self.messageVisible(),
|
editCommand: self => self.messageVisible(),
|
||||||
moveCommand: self => self.messageVisible(),
|
moveCommand: self => self.messageVisible(),
|
||||||
|
copyCommand: self => self.messageVisible(),
|
||||||
goUpCommand: self => !self.messageListOrViewLoading(),
|
goUpCommand: self => !self.messageListOrViewLoading(),
|
||||||
goDownCommand: self => !self.messageListOrViewLoading()
|
goDownCommand: self => !self.messageListOrViewLoading()
|
||||||
});
|
});
|
||||||
|
|
@ -253,13 +258,21 @@ export class MailMessageView extends AbstractViewRight {
|
||||||
currentMessage() && showMessageComposer([ComposeType.Draft, currentMessage()]);
|
currentMessage() && showMessageComposer([ComposeType.Draft, currentMessage()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
moveCommand(vm, event) {
|
moveOrCopy(vm, event, mode) {
|
||||||
if (vm && event?.preventDefault) {
|
if (vm && event?.preventDefault) {
|
||||||
stopEvent(event);
|
stopEvent(event);
|
||||||
}
|
}
|
||||||
this.actionsMenu().ddBtn.hide();
|
this.actionsMenu().ddBtn.hide();
|
||||||
AppUserStore.focusedState(ScopeFolderList);
|
AppUserStore.focusedState(ScopeFolderList);
|
||||||
moveAction(true);
|
moveAction(mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
moveCommand(vm, event) {
|
||||||
|
this.moveOrCopy(vm, event, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
copyCommand(vm, event) {
|
||||||
|
this.moveOrCopy(vm, event, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
setUnseen() {
|
setUnseen() {
|
||||||
|
|
@ -500,6 +513,17 @@ export class MailMessageView extends AbstractViewRight {
|
||||||
currentMessage().showExternalImages();
|
currentMessage().showExternalImages();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
showTracking() {
|
||||||
|
const msg = currentMessage(), body = msg?.body;
|
||||||
|
if (body && msg.hasTracking()) {
|
||||||
|
let attr = 'data-x-href-tracking';
|
||||||
|
body.querySelectorAll('a['+attr+']').forEach(node => node.href = node.getAttribute(attr));
|
||||||
|
// attr = 'data-x-src-tracking';
|
||||||
|
// body.querySelectorAll('img['+attr+']').forEach(node => node.src = node.getAttribute(attr));
|
||||||
|
this.nowTracking(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
whitelistText(txt) {
|
whitelistText(txt) {
|
||||||
let value = (SettingsUserStore.viewImagesWhitelist().trim() + '\n' + txt).trim();
|
let value = (SettingsUserStore.viewImagesWhitelist().trim() + '\n' + txt).trim();
|
||||||
/*
|
/*
|
||||||
|
|
@ -525,20 +549,19 @@ export class MailMessageView extends AbstractViewRight {
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
readReceipt() {
|
readReceipt() {
|
||||||
let oMessage = currentMessage()
|
let oMessage = currentMessage();
|
||||||
if (oMessage.readReceipt()) {
|
if (oMessage.readReceipt) {
|
||||||
Remote.request('SendReadReceiptMessage', iError => {
|
oMessage.flags.push('$mdnsent');
|
||||||
if (!iError) {
|
Remote.request('SendReadReceiptMessage',
|
||||||
oMessage.flags.push('$mdnsent');
|
iError => iError && oMessage.flags.remove('$mdnsent'),
|
||||||
// oMessage.flags.valueHasMutated();
|
{
|
||||||
|
messageFolder: oMessage.folder,
|
||||||
|
messageUid: oMessage.uid,
|
||||||
|
readReceipt: oMessage.readReceipt,
|
||||||
|
subject: i18n('READ_RECEIPT/SUBJECT', { SUBJECT: oMessage.subject() }),
|
||||||
|
plain: i18n('READ_RECEIPT/BODY', { 'READ-RECEIPT': AccountUserStore.email() })
|
||||||
}
|
}
|
||||||
}, {
|
);
|
||||||
messageFolder: oMessage.folder,
|
|
||||||
messageUid: oMessage.uid,
|
|
||||||
readReceipt: oMessage.readReceipt(),
|
|
||||||
subject: i18n('READ_RECEIPT/SUBJECT', { SUBJECT: oMessage.subject() }),
|
|
||||||
plain: i18n('READ_RECEIPT/BODY', { 'READ-RECEIPT': AccountUserStore.email() })
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -554,40 +577,45 @@ export class MailMessageView extends AbstractViewRight {
|
||||||
}
|
}
|
||||||
|
|
||||||
pgpDecrypt() {
|
pgpDecrypt() {
|
||||||
const oMessage = currentMessage();
|
const oMessage = currentMessage(),
|
||||||
|
data = oMessage.pgpEncrypted();
|
||||||
|
delete data.error;
|
||||||
PgpUserStore.decrypt(oMessage).then(result => {
|
PgpUserStore.decrypt(oMessage).then(result => {
|
||||||
if (result) {
|
if (!result) {
|
||||||
oMessage.pgpDecrypted(true);
|
|
||||||
if (result.data) {
|
|
||||||
MimeToMessage(result.data, oMessage);
|
|
||||||
oMessage.html() ? oMessage.viewHtml() : oMessage.viewPlain();
|
|
||||||
if (result.signatures?.length) {
|
|
||||||
oMessage.pgpSigned(true);
|
|
||||||
oMessage.pgpVerified({
|
|
||||||
signatures: result.signatures,
|
|
||||||
success: !!result.signatures.length
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// TODO: translate
|
// TODO: translate
|
||||||
alert('Decryption failed, canceled or not possible');
|
throw Error('Decryption failed, canceled or not possible');
|
||||||
|
}
|
||||||
|
oMessage.pgpDecrypted(true);
|
||||||
|
if (result.data) {
|
||||||
|
MimeToMessage(result.data, oMessage);
|
||||||
|
oMessage.html() ? oMessage.viewHtml() : oMessage.viewPlain();
|
||||||
|
if (result.signatures?.length) {
|
||||||
|
oMessage.pgpSigned({
|
||||||
|
signatures: result.signatures,
|
||||||
|
success: !!result.signatures.length
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(e => console.error(e));
|
.catch(e => {
|
||||||
|
data.error = e.message;
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
oMessage.pgpEncrypted(data);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pgpVerify(/*self, event*/) {
|
pgpVerify(/*self, event*/) {
|
||||||
const oMessage = currentMessage()/*, ctrl = event.target.closest('.openpgp-control')*/;
|
const oMessage = currentMessage()/*, ctrl = event.target.closest('.openpgp-control')*/;
|
||||||
PgpUserStore.verify(oMessage).then(result => {
|
PgpUserStore.verify(oMessage).then(result => {
|
||||||
if (result) {
|
if (result) {
|
||||||
oMessage.pgpVerified(result);
|
oMessage.pgpSigned(result);
|
||||||
} else {
|
} else {
|
||||||
alert('Verification failed or no valid public key found');
|
alert('Verification failed or no valid public key found');
|
||||||
}
|
}
|
||||||
/*
|
/*
|
||||||
if (result?.success) {
|
if (result?.success) {
|
||||||
i18n('OPENPGP/GOOD_SIGNATURE', {
|
i18n('CRYPTO/GOOD_SIGNATURE', {
|
||||||
USER: validKey.user + ' (' + validKey.id + ')'
|
USER: validKey.user + ' (' + validKey.id + ')'
|
||||||
});
|
});
|
||||||
message.getText()
|
message.getText()
|
||||||
|
|
@ -597,7 +625,8 @@ export class MailMessageView extends AbstractViewRight {
|
||||||
? keyIds.map(item => item?.toHex?.()).filter(v => v).join(', ')
|
? keyIds.map(item => item?.toHex?.()).filter(v => v).join(', ')
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
i18n('OPENPGP/ERROR', {
|
i18n('CRYPTO/ERROR', {
|
||||||
|
TYPE: 'OpenPGP',
|
||||||
ERROR: 'message'
|
ERROR: 'message'
|
||||||
}) + (additional ? ' (' + additional + ')' : '');
|
}) + (additional ? ' (' + additional + ')' : '');
|
||||||
}
|
}
|
||||||
|
|
@ -605,4 +634,68 @@ export class MailMessageView extends AbstractViewRight {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async smimeDecrypt() {
|
||||||
|
const message = currentMessage();
|
||||||
|
const addresses = message.from.concat(message.to, message.cc, message.bcc).map(item => item.email),
|
||||||
|
identity = IdentityUserStore.find(item => addresses.includes(item.email())),
|
||||||
|
data = message.smimeEncrypted(); // { partId: "1" }
|
||||||
|
if (data && identity) {
|
||||||
|
delete data.error;
|
||||||
|
let pass, params = { ...data }; // clone
|
||||||
|
params.folder = message.folder;
|
||||||
|
params.uid = message.uid;
|
||||||
|
// params.bodyPart = params.bodyPart?.raw;
|
||||||
|
params.certificate = identity.smimeCertificate();
|
||||||
|
params.privateKey = identity.smimeKey();
|
||||||
|
if (identity.smimeKeyEncrypted()) {
|
||||||
|
pass = await Passphrases.ask(identity,
|
||||||
|
i18n('SMIME/PRIVATE_KEY_OF', {EMAIL: identity.email()}),
|
||||||
|
'CRYPTO/DECRYPT'
|
||||||
|
);
|
||||||
|
if (!pass) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
params.passphrase = pass?.password;
|
||||||
|
}
|
||||||
|
Remote.post('SMimeDecryptMessage', null, params).then(response => {
|
||||||
|
if (response?.Result?.data) {
|
||||||
|
message.smimeDecrypted(true);
|
||||||
|
MimeToMessage(response.Result.data, message);
|
||||||
|
message.html() ? message.viewHtml() : message.viewPlain();
|
||||||
|
pass && pass.remember && Passphrases.handle(identity, pass.password);
|
||||||
|
if ('signed' in response.Result) {
|
||||||
|
message.smimeSigned(response.Result.signed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).catch(e => {
|
||||||
|
data.error = e.message
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
message.smimeEncrypted(data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
smimeVerify(/*self, event*/) {
|
||||||
|
const message = currentMessage(),
|
||||||
|
data = message.smimeSigned(); // { partId: "1", micAlg: "pgp-sha256" }
|
||||||
|
if (data) {
|
||||||
|
const params = { ...data }; // clone
|
||||||
|
params.folder = message.folder;
|
||||||
|
params.uid = message.uid;
|
||||||
|
params.bodyPart = data.bodyPart?.raw;
|
||||||
|
params.sigPart = data.sigPart?.bodyRaw;
|
||||||
|
Remote.post('SMimeVerifyMessage', null, params).then(response => {
|
||||||
|
if (response?.Result) {
|
||||||
|
if (response.Result.body) {
|
||||||
|
MimeToMessage(response.Result.body, message);
|
||||||
|
message.html() ? message.viewHtml() : message.viewPlain();
|
||||||
|
}
|
||||||
|
data.success = response.Result.success;
|
||||||
|
message.smimeSigned(data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -87,9 +87,8 @@ export class SystemDropDownUserView extends AbstractViewRight {
|
||||||
}
|
}
|
||||||
|
|
||||||
accountName() {
|
accountName() {
|
||||||
let email = AccountUserStore.email(),
|
const email = AccountUserStore.email();
|
||||||
account = AccountUserStore.find(account => account.email == email);
|
return AccountUserStore.find(account => account.email == email)?.label() || IDN.toUnicode(email);
|
||||||
return account?.name || email;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
settingsClick() {
|
settingsClick() {
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
const
|
const
|
||||||
qUri = path => doc.location.pathname.replace(/\/+$/,'') + '/?/' + path,
|
qUri = path => doc.location.pathname.replace(/\/+$/,'') + '/?/' + path,
|
||||||
eId = id => doc.getElementById('rl-'+id),
|
eId = id => doc.getElementById('rl-'+id),
|
||||||
admin = '1' == eId('app')?.dataset?.admin,
|
admin = '1' == eId('app').dataset.admin,
|
||||||
|
|
||||||
toggle = div => {
|
toggle = div => {
|
||||||
eId('loading').hidden = true;
|
eId('loading').hidden = true;
|
||||||
|
|
@ -69,7 +69,7 @@ window.rl = {
|
||||||
headers: {}
|
headers: {}
|
||||||
}, init);
|
}, init);
|
||||||
let asJSON = 1,
|
let asJSON = 1,
|
||||||
XToken = RL_APP_DATA.System?.token,
|
XToken = (RL_APP_DATA.System || {}).token,
|
||||||
object = {};
|
object = {};
|
||||||
if (postData) {
|
if (postData) {
|
||||||
init.method = 'POST';
|
init.method = 'POST';
|
||||||
|
|
|
||||||
|
|
@ -75,8 +75,8 @@
|
||||||
// create drag image from custom element or drag source
|
// create drag image from custom element or drag source
|
||||||
img = src.cloneNode(true);
|
img = src.cloneNode(true);
|
||||||
copyStyle(src, img);
|
copyStyle(src, img);
|
||||||
img._x = xOffset == null ? src.clientWidth / 2 : xOffset;
|
img._x = xOffset ?? src.clientWidth / 2;
|
||||||
img._y = yOffset == null ? src.clientHeight / 2 : yOffset;
|
img._y = yOffset ?? src.clientHeight / 2;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
7
dev/prototype.js
vendored
7
dev/prototype.js
vendored
|
|
@ -44,6 +44,13 @@
|
||||||
return template.content.firstChild;
|
return template.content.firstChild;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://github.com/tc39/proposal-regex-escaping
|
||||||
|
*/
|
||||||
|
if (!RegExp.escape){
|
||||||
|
RegExp.escape = s => String(s).replace(/[\\^$*+?.()|[\]{}]/g, '\\$&');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Every time the function is executed,
|
* Every time the function is executed,
|
||||||
* it will delay the execution with the given amount of milliseconds.
|
* it will delay the execution with the given amount of milliseconds.
|
||||||
|
|
|
||||||
106
examples/crypto/3140898548F06E97-private.asc
Normal file
106
examples/crypto/3140898548F06E97-private.asc
Normal file
|
|
@ -0,0 +1,106 @@
|
||||||
|
-----BEGIN PGP PRIVATE KEY BLOCK-----
|
||||||
|
|
||||||
|
lQdGBGNyBMoBEAC+x1PNa+DHQA+CJ0pznECiL11cmTzfkas6ahPNTYqadnA0dab6
|
||||||
|
w81tQ6Q/QH92WRbQnqfGqtI+LoyHsga4Abspqj5rKTprC06f/Q5TCF4KS7gRmVUW
|
||||||
|
8KhTtlIWv9aA+0aTNcjT/OPr8W5l7fb/T/dPHo9Pp3929XjVtVlNhANPRT6h7/M7
|
||||||
|
PzrOVT0mwhQnbZYZndD+PpU4MgH/TuKwxh0cI+IrpGZKCNR5ZYeLe3C9R8qojzrz
|
||||||
|
KxWFaj3Tev6qDDbqIAOp8r+fSC5TZPVtuvcqEZDtr9FLDq2oEgpWY+qKV7HiVo8h
|
||||||
|
lV4on0syIGOCd+SUW9ZS7qcb8qOPynvVzur4DTamrOdOqNd3ZO9hHh241JdGOdzw
|
||||||
|
eb9ASnvbtM52DW8LrfzGrtTc6HkzFHiqluwAS9sz99bxdlmT9zcmVDxYoQrbU3ML
|
||||||
|
ImZjwZZZedca/dDyNLLjehrViC9hI8rK2js6ndBUy9bvvxLtzpJA3K/urAlMsvGU
|
||||||
|
G1CRpSjys4XQnEJftxH+RssCi8N3mc3dV4YRDLKAHu+DU7d3ytUW82bkIZuYz0t3
|
||||||
|
umt45h2dOdgxqam6I8u5t3Ry72vtkaQciaitTBBa2yWxaoi1ib2rCwofVddW65QK
|
||||||
|
kCDsjV8GeS+t5UMiB28/OOiWSlCWEwdXVew0IVXSZao4/R3r2h+mVl8pVQARAQAB
|
||||||
|
/gcDArr2sIgJEqz269dc2L7650YxaMd6U+DTHOZLXjEOXI8vzgR+Z4dkKVC/2a4s
|
||||||
|
qdjSLKDV/DkhTbvJun7DqRlwqwuNp/U3/+K31iF9B25xqSWdDy8+iX0hrar2Rt4E
|
||||||
|
U+pAkfu+hrC+t4c2f/20tIUSocNKXZ5D56iORqPMbnwDu6nuwdYmnVMHisrf+aKe
|
||||||
|
/F2WmzivQ2ZLSlFTVU6fQoqMDPZxPHttD7Pm4BVuEvF8KPiOiwtChQvvO09DYCK/
|
||||||
|
EDRJWn7dDP1CoPVge5LntVN8xbtTG2Fi0DrYrLqF1mH3RoBn61KUo1wUOz+zh5F6
|
||||||
|
0jHIjyscm+SIYPVTsnEVEBsTTsMhPj5vMEKD5qqbrD6cBSsJ25awt7FzovEI+5EK
|
||||||
|
nl7xZAXvC2NxS+TNFe4hpsfD/Q/UYyOfV5cqQ+6O9m7/Z38xDvgE/oeX6eHmMvJk
|
||||||
|
GfFAj51I0SNZq71QPqQALG1M5d5adw5WAPWHAgke2xB50dPm7prUIPUXF5zm2+8c
|
||||||
|
KE6oOq9gQyRVL1nXVBXyDpc3a4vXzz6VNvQlBNLXc7wrO+/HvUc2y9qnWm+IqAvd
|
||||||
|
JRZvvX4Es/85KUuhPH083XLFfkuSP3lVPaCEppdvDwVom7iggVU0aw8uhJ66qz8S
|
||||||
|
w6oBl9B/k3bD9VNEGtlo3bXfv4fyFDBwX8eorhcdLv8TnbalVecOHY4BIZHkWaCZ
|
||||||
|
8PqT+GjNhEEgQDxREY0bBrF+KYE+GtJe+2bwyFTQaioszeaaRmN2WMHZaLn+04nH
|
||||||
|
Iu8hpABejerOPPY2QGPGhhggQFS45MjOd/2aBGiAOG1Tyia8lxVReb0IGat0Ifm+
|
||||||
|
sNZBI26MfYuscsapfOp0obIgGL6CKL30IByiYTAFr24167d87wMt0J709S4o+G90
|
||||||
|
vdjsQZv5oHCL2t1F2n4Ib6P0ZicRn7bmO2pfwbQOCxNBMnLfK170QmUBGbBwJTse
|
||||||
|
9zaPOIPoBp1yQz6KC6wzyVLdACHPntEdaZA88SJJv3uwcK/WsiM+uRE38FJ3q4AX
|
||||||
|
WNBx1C+HvV3Ybocr6ebhDEj1H0GRRWs5M/I7309tc9waB2O+R8MsMUaCnFLDippo
|
||||||
|
igBg8ZNIw4rPuLvGRQqUb+VAtKpr2MX4r00heHFSB9joaBoyHEEZCLiAxzk83Jvc
|
||||||
|
fY8KeJGA07T50q1BseISTZuXdxLNM5r6aZcfk3/zuURs7oqKBSxH8VGfGQxocB1i
|
||||||
|
4rEAodCKjxVVy1L2YRj0kZMgcUgrbQKLzEcsEmkoehiiVfbc4MZRXLM+yvVRRAhi
|
||||||
|
PABALLqI0oxhbqya1rGo15kvHKzoJqcsudLHtjkqXlrfxgFtoeEuHPCq+0Nmsgnj
|
||||||
|
iuxW6WLSEVH4DPIci6nnMnQhS0357i3a8LAKteIX9K/0A2RgjMY7FGgYXnGj8JHM
|
||||||
|
C0nkYQ95lTb5tF0ZzTtQIf4+gG7gK5kG7PJCzbBQr9a8ll6CjpqbZmMwwUPEwCvv
|
||||||
|
W9U/Ds+Nf0AaLVoJdNNw/GLru4Ms90SjgITQ49IXlaX0ict5Vpavyu9I23DIlLo5
|
||||||
|
r+3JkgCFkYjB6PrLmjzMN/REJ/I6iG0iXUm9tGMJ5azgo2bJmn+VxI1jdvySNxSI
|
||||||
|
AWsAuZvLpL6qXKQKVYJLKoZACgPq9fyncvPePIUAhVJdAv/zjoJ31zrskEwbrazR
|
||||||
|
Mmf/3Rm7GGV3hSPqkYRfwTg2/Ef6c9VumevRoWdjxOIOaITueOxb4evzsBGqZKLr
|
||||||
|
BW05fILbmeCwL4EtjP/ykC8r+ndbqG04KBYGM9rWkznn0NTvbRQi0020GURlbW8g
|
||||||
|
PGRlbW9Ac25hcHB5bWFpbC5ldT6JAjUEEAEIACkFAmNyBMsGCwkHCAMCCRAxQImF
|
||||||
|
SPBulwQVCAoCAxYCAQIZAQIbAwIeAQAAQGsP/3iQz6K9Q0bDg0PNSBzP/6v9j5GG
|
||||||
|
A5k5piiudlIcZcfSDgGmFwVsknJnGjaPXp62/rnFIUm0oHEgkP36eyK15Cycd+qn
|
||||||
|
KWSlaeWMAcv70tKJDQjweQyyM9896lCXKkdM7SZNvBLfjh9YQdBIayb9QAbk+4Mf
|
||||||
|
hIQ+miybS4PUeoWkx2RR5cFEiOOMU2+HwgNbcbtzFRRuMpho7mI2E8lTuWWokKAx
|
||||||
|
ENeNMeuzcRW0FoYzrURvxBtbe0P6fr/673RVs8mdfxumUIyCI0OgaouII0EGqs6R
|
||||||
|
W+MeMVMmZ5fpFPXcwYzNQJ401VpjL1MQyAIRqTaIX7EERzp6tAtgdqDEJzHYwRtC
|
||||||
|
n/aAmtCnauNw9rmoakDueLIRZ9FaU4OhYHv6g30FEMA+hrTxLNYs696fbXoB2/4u
|
||||||
|
KkOtY8pC6D5nvihBkcU1UEd5w4O8HM1V4Vn3ZXAZBY5Y66v/xSrPorDFc4gSXmOq
|
||||||
|
NgZE6/YMFVmB95mb0WNaKT1Xu8ZFZILAMZseNYd1YrxYg/OdkOHcoO+4h9GFu+Rq
|
||||||
|
5C/1BbAqrMssf+st+rRolhJ4TcgtdbCZzm5BO2ljnWCqEa3dciTU4jdPrgWi3Vpf
|
||||||
|
OcX42cmK45H18XbN7r+WbCsEDGmOMK4daGF+417hivREWbsrrEPqxp9FvCci6y8N
|
||||||
|
iq40mUCT+eVfFHGRnQdGBGNyBMoBEADWxp5f4mhqm6GkUpJJ12fKHsUFb3QNr5Kb
|
||||||
|
UkDXXd+vqD7mMl/28mAd15BzAIeQD0kawN3/cEUbc+X1tqSZGbVlUGNy0B6NHsdd
|
||||||
|
OvqBQQ41bBFTlchk7cORit1KYdSyg0QKTzJ90QrMe7/xL+LoM8QrrJjta4tEqZ8p
|
||||||
|
MQFeqGotnPW4kEQCKA2L8z9AQNeErwTUTL0GZAz8sY3ozCRvzKOMq+MCOAZNc0da
|
||||||
|
xk6JCtNrySSCefiLsFWDzk1CzbdwjNCzENPmUf6bRISULHuXr3EFNWz5XB0UHn3O
|
||||||
|
QezmZ+WFGTG63Kd5rsHHO6mZI4Fh+B9H3weF1WkYA+RNbAmhTTjTHbsuZ+tQnAOU
|
||||||
|
SmArQKLFysVg3ieFQSLrl7o1CceNG63nscR9nmtrcte5l5UJRMqnGP/nMkl0DvMo
|
||||||
|
0DLb2RykytwFzvZBOW2q4Wn/rgsDSeKoB/NCTHWRL/hYKkk3Q3iys2cJ8XK2datP
|
||||||
|
NPWfB4pl+M8ik2QGU7y3NZVN+KOacX7/+oNACU0Azhw2+1thFfpOtfYsm5/zVHyS
|
||||||
|
MtEWb3d3BUmXb++JGDWkeRMLxFjmLNYcVvFljeqfRmLjid76FAKw6dPv4f7ipDLa
|
||||||
|
z+I+qvzJEIdWtek+E2tOoWdL7+T+mmlkw2S2+eutWxsA1RZvuKLaDGkFxm6rT7z6
|
||||||
|
4KdtDqcq0QARAQAB/gcDAizfuR2QYFd36+tujgnFtbXOqjPLHgZ9D+4PlTV9tfJy
|
||||||
|
srqR7/9951k2wkMMJ33NrntZ5iVsRgEBaBf7ZAABBrl+LFOARYsO/wxe+ilf4fNq
|
||||||
|
49e4hvE57CiV4YAcGyDu2/EOBmIPQcIbUUUJLylD2M8fAeMrO9L3IX6qF+Z5rHdV
|
||||||
|
7A+45ZNT743Yy6iQzFllNkBGOquu8PUUPQHTomgZl8gX+UETskb3JSihQiwyZZmG
|
||||||
|
RqaWwMikgKSYYtPBluyXQMBD48PRKxtMQOI3S+fFFP2IIKXar1q4RHZsBgwYrRlf
|
||||||
|
PfKLnICZLedYSBgL1NiymLlKsmQpt7PfDRQfyMeRzP0bYv2295sczKX0oyxfRGdF
|
||||||
|
fmtj4P83XiTfJvW3xbit3vTNVWzQQtXdYc5GPMc+CXeH/lzaefbuWRlxv4JLawnK
|
||||||
|
gGTmKHa9DaAZOQ9Cq8wbzitnobTeK8JDPRrFrZ+pksDxk2XJCrddR5kmYdXYGygQ
|
||||||
|
brcLeENVM2sJ9T38qMFJz0Oj7SaYH3l/ErYJiyQgPIpHETqefSj7ZZ/o0fnZP400
|
||||||
|
2cjGoHIVDH/D/kW0clfR/WtNavAzT84tUPBgoXVjMwInZngD39XhQKMeWAEpoOAX
|
||||||
|
yPdu0NisYo1jC+ILMpcnHuAAWhdaKNXoTbPR3PFiKDbdRIEN1+9J3SKA+nY9UeOn
|
||||||
|
klEnPCZ7/1LWRjgidTsDwHHyDuEmYe0luRl/YAvzyiiQLWJKUerAod0EW3lxTwQ+
|
||||||
|
ukMBMqzHjF/rpn/l5A84K4Z3YnAare0C97ldjUWjxYA2UKBYq6dB4R7Jif/6cHcx
|
||||||
|
ReqCyPqW9S4fbZG1BgwdhEigbo+DU0DmcITX9nW7r6U4ZwMGKsFPU+7ZIZmwfxPy
|
||||||
|
aRqqzKr9nw0/DbH2xLUN+tjs4QGpzXIUYxgShPt9SvfO0I79nW/6nVsKzQ3Vq2Id
|
||||||
|
SI8vLNH/lD0+C5bsmCwwCAtkkbHkeUUtyj/yve2bsCtzRFA5XlfibnpT9kPe6D4G
|
||||||
|
qdBI9Y9Xme3NUYW2Jw1kLbUfHhwHD/KoiRWb6znuGtkcWb7jx0YXNtoObTmJpoiq
|
||||||
|
uruTMk7tZRj8BqILOm1Hob6h24uyZBro4v5nQukii67Cuf7I99X0P4Dpfxc7Mmnk
|
||||||
|
ebZoRsMWHCgASir1eG4KJk0SekU4/aYJiZhK0hat0G5l4d4oi1s4ajzMAAbH97FB
|
||||||
|
fC220wsmZ3YbVDN8tAkukCuSW0kSYdYfXN1iHpqb9W6rodNyqMNqrSQSmZICqDZM
|
||||||
|
Mg+JI7YwcPsMwMr94iOaCy7W+jsAb6Eon7CMztsAMNKdDbCJxKF0fwxB8QfD4mVg
|
||||||
|
E4vT44zWnWCvB2XSHhMz0iJJRVlMFqbTdhMS8L26BLBzJfAX4OCUNJdpm6T1gVUR
|
||||||
|
X56v36FMQH+Iz96whc5psLVsjLiizZGLIJy2FBRTtFtGkdNJOyPaWLGUO2bDeWI7
|
||||||
|
Rd9CDiWZD6koVtZT2JB3mk7N4ASQ0KttqaMDBRoIqJlpdjqSKtJxSjPHaNBDjert
|
||||||
|
jCDE6OwE401/Mnh9AeT5+vr6OVwmk/wbZoC+X9jXzfuqMGqvo6ElLPbSKJpIWtnE
|
||||||
|
0aqhfjBL+nDIwaqNZAaHZd/sFmVkDh1MJLKgWdCEWWHdReXWKIlSRcNkXMGZ93NQ
|
||||||
|
kUB1sqp1qmfK56SS90iJAHGAOKbac15dTcW1J/YJz9ZXr4xgEZAuiFCUFMWAEVOo
|
||||||
|
sCBs1rP2pUOqsQLiLjNleQ2Grz52aPNuCIajQwdjl8K6Ly8EXHcp9qznB6AP5+59
|
||||||
|
YdwhmUiJAh8EGAEIABMFAmNyBMwJEDFAiYVI8G6XAhsMAADVEg/8D502NBHE0cnT
|
||||||
|
FuBJIbz1/lVc272ka0MMaknCHc8OV451HnIJ0foqNOqd1hEtUP1dk+8krgR+Izdo
|
||||||
|
322WlR7t7LAPHmh7As1iBNHB43+oas0nXcrnnAr7kSGB3a0AVxP62f4ogUIgZkEc
|
||||||
|
3gRz7QvzUvpVVQSd1SONfnjprEu5M22gMKZT93CkzsIYMumuCB7nlRJ6Fjy9zWem
|
||||||
|
TOCTDpnfQHgpWBu4Zo6EUKw4SxMhPsT6ngfdAAQtp9Eyxg6Mv0Oj4zYAhu7OMmGV
|
||||||
|
MWmouHTAluXEa/BjH+W2wa4dk2wZXBwZi/y1ZYVM4hFJcNk74XymsJbZAyX4aIAP
|
||||||
|
JeSzEyrYltJjBjMv/4DSF5ppznzpl7G4OrojqfngHxABwd5zl87lZ2Pde0XFZnK0
|
||||||
|
Ltw7K+pMYswqJlrH1/bKy1oYtrBFlvF2MdLfHJzClr3IbBB1qEaUxz1JMm1AXz+c
|
||||||
|
y90ESbjZTvf6a0PQmxoPMyBqXryyt0ZEcoccTz02X1VWfCGgpqf4oVdxhVwUq6oV
|
||||||
|
LV8pPd+oaAzEcUrQlvVMgSCJGpqzibYRNVAVWUwI3v23YzXip8v9hLabSY7B1PRw
|
||||||
|
eojXyE9btmOUxD4AoJ949b2zklg/faPqBgbHbUeoN6yvWysFcROAdnGA/wHLv/zI
|
||||||
|
YXYPCv6njzAv1+fTgemAZYyomQ1qhVg=
|
||||||
|
=a9os
|
||||||
|
-----END PGP PRIVATE KEY BLOCK-----
|
||||||
50
examples/crypto/3140898548F06E97-public.asc
Normal file
50
examples/crypto/3140898548F06E97-public.asc
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||||
|
|
||||||
|
mQINBGNyBMoBEAC+x1PNa+DHQA+CJ0pznECiL11cmTzfkas6ahPNTYqadnA0dab6
|
||||||
|
w81tQ6Q/QH92WRbQnqfGqtI+LoyHsga4Abspqj5rKTprC06f/Q5TCF4KS7gRmVUW
|
||||||
|
8KhTtlIWv9aA+0aTNcjT/OPr8W5l7fb/T/dPHo9Pp3929XjVtVlNhANPRT6h7/M7
|
||||||
|
PzrOVT0mwhQnbZYZndD+PpU4MgH/TuKwxh0cI+IrpGZKCNR5ZYeLe3C9R8qojzrz
|
||||||
|
KxWFaj3Tev6qDDbqIAOp8r+fSC5TZPVtuvcqEZDtr9FLDq2oEgpWY+qKV7HiVo8h
|
||||||
|
lV4on0syIGOCd+SUW9ZS7qcb8qOPynvVzur4DTamrOdOqNd3ZO9hHh241JdGOdzw
|
||||||
|
eb9ASnvbtM52DW8LrfzGrtTc6HkzFHiqluwAS9sz99bxdlmT9zcmVDxYoQrbU3ML
|
||||||
|
ImZjwZZZedca/dDyNLLjehrViC9hI8rK2js6ndBUy9bvvxLtzpJA3K/urAlMsvGU
|
||||||
|
G1CRpSjys4XQnEJftxH+RssCi8N3mc3dV4YRDLKAHu+DU7d3ytUW82bkIZuYz0t3
|
||||||
|
umt45h2dOdgxqam6I8u5t3Ry72vtkaQciaitTBBa2yWxaoi1ib2rCwofVddW65QK
|
||||||
|
kCDsjV8GeS+t5UMiB28/OOiWSlCWEwdXVew0IVXSZao4/R3r2h+mVl8pVQARAQAB
|
||||||
|
tBlEZW1vIDxkZW1vQHNuYXBweW1haWwuZXU+iQI1BBABCAApBQJjcgTLBgsJBwgD
|
||||||
|
AgkQMUCJhUjwbpcEFQgKAgMWAgECGQECGwMCHgEAAEBrD/94kM+ivUNGw4NDzUgc
|
||||||
|
z/+r/Y+RhgOZOaYornZSHGXH0g4BphcFbJJyZxo2j16etv65xSFJtKBxIJD9+nsi
|
||||||
|
teQsnHfqpylkpWnljAHL+9LSiQ0I8HkMsjPfPepQlypHTO0mTbwS344fWEHQSGsm
|
||||||
|
/UAG5PuDH4SEPposm0uD1HqFpMdkUeXBRIjjjFNvh8IDW3G7cxUUbjKYaO5iNhPJ
|
||||||
|
U7llqJCgMRDXjTHrs3EVtBaGM61Eb8QbW3tD+n6/+u90VbPJnX8bplCMgiNDoGqL
|
||||||
|
iCNBBqrOkVvjHjFTJmeX6RT13MGMzUCeNNVaYy9TEMgCEak2iF+xBEc6erQLYHag
|
||||||
|
xCcx2MEbQp/2gJrQp2rjcPa5qGpA7niyEWfRWlODoWB7+oN9BRDAPoa08SzWLOve
|
||||||
|
n216Adv+LipDrWPKQug+Z74oQZHFNVBHecODvBzNVeFZ92VwGQWOWOur/8Uqz6Kw
|
||||||
|
xXOIEl5jqjYGROv2DBVZgfeZm9FjWik9V7vGRWSCwDGbHjWHdWK8WIPznZDh3KDv
|
||||||
|
uIfRhbvkauQv9QWwKqzLLH/rLfq0aJYSeE3ILXWwmc5uQTtpY51gqhGt3XIk1OI3
|
||||||
|
T64Fot1aXznF+NnJiuOR9fF2ze6/lmwrBAxpjjCuHWhhfuNe4Yr0RFm7K6xD6saf
|
||||||
|
RbwnIusvDYquNJlAk/nlXxRxkbkCDQRjcgTKARAA1saeX+JoapuhpFKSSddnyh7F
|
||||||
|
BW90Da+Sm1JA113fr6g+5jJf9vJgHdeQcwCHkA9JGsDd/3BFG3Pl9bakmRm1ZVBj
|
||||||
|
ctAejR7HXTr6gUEONWwRU5XIZO3DkYrdSmHUsoNECk8yfdEKzHu/8S/i6DPEK6yY
|
||||||
|
7WuLRKmfKTEBXqhqLZz1uJBEAigNi/M/QEDXhK8E1Ey9BmQM/LGN6Mwkb8yjjKvj
|
||||||
|
AjgGTXNHWsZOiQrTa8kkgnn4i7BVg85NQs23cIzQsxDT5lH+m0SElCx7l69xBTVs
|
||||||
|
+VwdFB59zkHs5mflhRkxutynea7BxzupmSOBYfgfR98HhdVpGAPkTWwJoU040x27
|
||||||
|
LmfrUJwDlEpgK0CixcrFYN4nhUEi65e6NQnHjRut57HEfZ5ra3LXuZeVCUTKpxj/
|
||||||
|
5zJJdA7zKNAy29kcpMrcBc72QTltquFp/64LA0niqAfzQkx1kS/4WCpJN0N4srNn
|
||||||
|
CfFytnWrTzT1nweKZfjPIpNkBlO8tzWVTfijmnF+//qDQAlNAM4cNvtbYRX6TrX2
|
||||||
|
LJuf81R8kjLRFm93dwVJl2/viRg1pHkTC8RY5izWHFbxZY3qn0Zi44ne+hQCsOnT
|
||||||
|
7+H+4qQy2s/iPqr8yRCHVrXpPhNrTqFnS+/k/pppZMNktvnrrVsbANUWb7ii2gxp
|
||||||
|
BcZuq0+8+uCnbQ6nKtEAEQEAAYkCHwQYAQgAEwUCY3IEzAkQMUCJhUjwbpcCGwwA
|
||||||
|
ANUSD/wPnTY0EcTRydMW4EkhvPX+VVzbvaRrQwxqScIdzw5XjnUecgnR+io06p3W
|
||||||
|
ES1Q/V2T7ySuBH4jN2jfbZaVHu3ssA8eaHsCzWIE0cHjf6hqzSddyuecCvuRIYHd
|
||||||
|
rQBXE/rZ/iiBQiBmQRzeBHPtC/NS+lVVBJ3VI41+eOmsS7kzbaAwplP3cKTOwhgy
|
||||||
|
6a4IHueVEnoWPL3NZ6ZM4JMOmd9AeClYG7hmjoRQrDhLEyE+xPqeB90ABC2n0TLG
|
||||||
|
Doy/Q6PjNgCG7s4yYZUxaai4dMCW5cRr8GMf5bbBrh2TbBlcHBmL/LVlhUziEUlw
|
||||||
|
2TvhfKawltkDJfhogA8l5LMTKtiW0mMGMy//gNIXmmnOfOmXsbg6uiOp+eAfEAHB
|
||||||
|
3nOXzuVnY917RcVmcrQu3Dsr6kxizComWsfX9srLWhi2sEWW8XYx0t8cnMKWvchs
|
||||||
|
EHWoRpTHPUkybUBfP5zL3QRJuNlO9/prQ9CbGg8zIGpevLK3RkRyhxxPPTZfVVZ8
|
||||||
|
IaCmp/ihV3GFXBSrqhUtXyk936hoDMRxStCW9UyBIIkamrOJthE1UBVZTAje/bdj
|
||||||
|
NeKny/2EtptJjsHU9HB6iNfIT1u2Y5TEPgCgn3j1vbOSWD99o+oGBsdtR6g3rK9b
|
||||||
|
KwVxE4B2cYD/Acu//Mhhdg8K/qePMC/X59OB6YBljKiZDWqFWA==
|
||||||
|
=0ljX
|
||||||
|
-----END PGP PUBLIC KEY BLOCK-----
|
||||||
16
examples/crypto/5F3A5CDC09AD8AE3-private.asc
Normal file
16
examples/crypto/5F3A5CDC09AD8AE3-private.asc
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
-----BEGIN PGP PRIVATE KEY BLOCK-----
|
||||||
|
|
||||||
|
lIYEYfQDqBYJKwYBBAHaRw8BAQdAy+llquGb/U4M0kD2xyJoQ2pwlDN02C7XD906
|
||||||
|
7I8zdB3+BwMC7y2sGFhuzTHldCm0gfdOH3H2PkmhDfM/OSTYGVEu1mKInC3xmp2F
|
||||||
|
HmC2wUd+yM+hzMrQMgHn61iT9CHDeunUhQqG8PoXfAUAL7syGiXQ0rQZZGVtbyA8
|
||||||
|
ZGVtb0BzbmFwcHltYWlsLmV1PoiMBBAWCgAdBQJh9AOoBAsJBwgDFQgKBBYAAgEC
|
||||||
|
GQECGwMCHgEAIQkQXzpc3AmtiuMWIQQsIj8g6irbTLaPgdlfOlzcCa2K4/nPAP4u
|
||||||
|
UrWr39wv+YKsNcLwHwOpljyu59iHOXA3halUbVCeJwD/dY6JXCwMDgG+BmurPcJh
|
||||||
|
S/S8Q6fjlN9hUi/za3acYASciwRh9AOoEgorBgEEAZdVAQUBAQdAvXl+RCkqtUqN
|
||||||
|
VQ3Fj3bFTZjZOeNlI3ibK2eN6EjlnwcDAQgH/gcDAjAzqrbZEDg05XxnZqG/vHCg
|
||||||
|
1bJ/OofU+0oYZy6DHhAVDa5IhNLkC7kDeA8gXtQnNVuOd5kSIWvLcVRMZZval8iI
|
||||||
|
Xfrt037sAAciOIft1uSIeAQYFggACQUCYfQDqAIbDAAhCRBfOlzcCa2K4xYhBCwi
|
||||||
|
PyDqKttMto+B2V86XNwJrYrjolQBAMqdz8VkgMYjM7tinwUUTe4JjZoCsuhHPN6S
|
||||||
|
pQLd/UzKAQDgRlbAdrl042/nJcdrBrQz3+wVzkaF0ehvihBf4/tfDw==
|
||||||
|
=5A1c
|
||||||
|
-----END PGP PRIVATE KEY BLOCK-----
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue