Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/nextcloud/mail.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristoph Wurst <christoph@winzerhof-wurst.at>2019-10-10 14:36:35 +0300
committerChristoph Wurst <christoph@winzerhof-wurst.at>2020-01-08 16:01:06 +0300
commit150716df3480175ac223f59f9349eba8cedf6524 (patch)
tree4151035d6b00aba594799fbb4c1a4b5c935c206d
parentb57fa26fc1401cf3afc4289e35c6ed5d0cc863b4 (diff)
Use KItinerary to extract information from emails and attachments
Signed-off-by: Christoph Wurst <christoph@winzerhof-wurst.at>
-rw-r--r--.eslintrc.js1
-rw-r--r--composer.json15
-rw-r--r--composer.lock407
-rwxr-xr-xlib/Controller/MessagesController.php13
-rw-r--r--lib/IMAP/MessageMapper.php132
-rw-r--r--lib/Integration/KItinerary/ItineraryExtractor.php88
-rw-r--r--lib/Integration/Psr/LoggerAdapter.php158
-rw-r--r--lib/Model/IMAPMessage.php3
-rw-r--r--lib/Service/ItineraryService.php106
-rw-r--r--package-lock.json43
-rw-r--r--package.json1
-rw-r--r--src/components/Itinerary.vue71
-rw-r--r--src/components/Message.vue9
-rw-r--r--src/components/itinerary/CalendarImport.vue76
-rw-r--r--src/components/itinerary/EventReservation.vue162
-rw-r--r--src/components/itinerary/FlightReservation.vue177
-rw-r--r--src/components/itinerary/TrainReservation.vue185
-rw-r--r--src/service/DAVService.js3
-rw-r--r--tests/Integration/KItinerary/ItineraryExtractorTest.php103
-rw-r--r--tests/Service/ItineraryServiceTest.php138
-rw-r--r--tests/Unit/Controller/MessagesControllerTest.php6
21 files changed, 1829 insertions, 68 deletions
diff --git a/.eslintrc.js b/.eslintrc.js
index 7354f2593..ea38b3f81 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -27,6 +27,7 @@ module.exports = {
expect: true,
OC: true,
OCA: true,
+ OCP: true,
t: true,
__webpack_public_path__: true,
__webpack_nonce__: true,
diff --git a/composer.json b/composer.json
index 4eaa8ecbc..87a2e8202 100644
--- a/composer.json
+++ b/composer.json
@@ -17,6 +17,14 @@
},
"require": {
"php": ">=7.2",
+ "arthurhoaro/favicon": "^1.2",
+ "cerdic/css-tidy": "v1.7.1",
+ "christophwurst/kitinerary": "^0.2",
+ "christophwurst/kitinerary-bin": "^0.2",
+ "christophwurst/kitinerary-flatpak": "^0.2",
+ "ezyang/htmlpurifier": "4.12.0",
+ "gravatarphp/gravatar": "^2.0",
+ "kwi/urllinker": "dev-bleeding",
"pear-pear.horde.org/horde_date": "^2.4.1@stable",
"pear-pear.horde.org/horde_exception": "^2.0.8@stable",
"pear-pear.horde.org/horde_imap_client": "^2.29.16@stable",
@@ -29,13 +37,10 @@
"pear-pear.horde.org/horde_text_flowed": "^2.0.3@stable",
"pear-pear.horde.org/horde_util": "^2.5.8@stable",
"pear-pear.horde.org/horde_smtp": "^1.9.5@stable",
- "cerdic/css-tidy": "v1.7.1",
- "ezyang/htmlpurifier": "4.12.0",
- "kwi/urllinker": "dev-bleeding",
- "gravatarphp/gravatar": "^2.0",
- "arthurhoaro/favicon": "^1.2"
+ "psr/log": "^1"
},
"require-dev": {
+ "roave/security-advisories": "dev-master",
"christophwurst/nextcloud": "v17.0.2",
"christophwurst/nextcloud_testing": "0.9.1",
"phan/phan": "^2.0"
diff --git a/composer.lock b/composer.lock
index 08595cb58..2605ef153 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "ea48c3a9078d8b441b0d2078ff6039df",
+ "content-hash": "156f493170c7639a0e4cdcb0a6ecc92a",
"packages": [
{
"name": "arthurhoaro/favicon",
@@ -102,6 +102,118 @@
"time": "2019-09-14T17:59:23+00:00"
},
{
+ "name": "christophwurst/kitinerary",
+ "version": "0.2.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/ChristophWurst/kitinerary.git",
+ "reference": "166abd7af84632130d8c74fd80b2225631ee95c6"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/ChristophWurst/kitinerary/zipball/166abd7af84632130d8c74fd80b2225631ee95c6",
+ "reference": "166abd7af84632130d8c74fd80b2225631ee95c6",
+ "shasum": ""
+ },
+ "require": {
+ "ext-json": "*"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "ChristophWurst\\KItinerary\\": "/src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "AGPL-3.0-or-later"
+ ],
+ "authors": [
+ {
+ "name": "Christoph Wurst",
+ "email": "christoph@winzerhof-wurst.at"
+ }
+ ],
+ "description": "KItinerary adapter",
+ "time": "2019-12-04T14:51:41+00:00"
+ },
+ {
+ "name": "christophwurst/kitinerary-bin",
+ "version": "0.2.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/ChristophWurst/kitinerary-bin.git",
+ "reference": "76ad3cfcf6fd896e6d3ffc560ffa17bb8efbd7e4"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/ChristophWurst/kitinerary-bin/zipball/76ad3cfcf6fd896e6d3ffc560ffa17bb8efbd7e4",
+ "reference": "76ad3cfcf6fd896e6d3ffc560ffa17bb8efbd7e4",
+ "shasum": ""
+ },
+ "require": {
+ "christophwurst/kitinerary": "^0.2.0",
+ "ext-json": "*",
+ "php": "^7.1",
+ "psr/log": "^1.1"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "ChristophWurst\\KItinerary\\Bin\\": "/src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "LGPL-3.0-or-later"
+ ],
+ "authors": [
+ {
+ "name": "Christoph Wurst",
+ "email": "christoph@winzerhof-wurst.at"
+ }
+ ],
+ "description": "KItinerary binary executable",
+ "time": "2019-12-04T14:56:35+00:00"
+ },
+ {
+ "name": "christophwurst/kitinerary-flatpak",
+ "version": "0.2.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/ChristophWurst/kitinerary-flatpak.git",
+ "reference": "1bc6b8304c68f8656c5f1b7bf7e74a3c465e211c"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/ChristophWurst/kitinerary-flatpak/zipball/1bc6b8304c68f8656c5f1b7bf7e74a3c465e211c",
+ "reference": "1bc6b8304c68f8656c5f1b7bf7e74a3c465e211c",
+ "shasum": ""
+ },
+ "require": {
+ "christophwurst/kitinerary": "0.2.0",
+ "ext-json": "*"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "ChristophWurst\\KItinerary\\Flatpak\\": "/src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "AGPL-3.0-or-later"
+ ],
+ "authors": [
+ {
+ "name": "Christoph Wurst",
+ "email": "christoph@winzerhof-wurst.at"
+ }
+ ],
+ "description": "KItinerary Flatpak binding",
+ "time": "2019-12-04T14:55:14+00:00"
+ },
+ {
"name": "ezyang/htmlpurifier",
"version": "v4.12.0",
"source": {
@@ -852,6 +964,53 @@
"LGPL-2.1"
],
"description": "A library that provides functionality useful for all kind of applications."
+ },
+ {
+ "name": "psr/log",
+ "version": "1.1.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/log.git",
+ "reference": "446d54b4cb6bf489fc9d75f55843658e6f25d801"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/log/zipball/446d54b4cb6bf489fc9d75f55843658e6f25d801",
+ "reference": "446d54b4cb6bf489fc9d75f55843658e6f25d801",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.1.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Log\\": "Psr/Log/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "http://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interface for logging libraries",
+ "homepage": "https://github.com/php-fig/log",
+ "keywords": [
+ "log",
+ "psr",
+ "psr-3"
+ ],
+ "time": "2019-11-01T11:05:21+00:00"
}
],
"packages-dev": [
@@ -1972,16 +2131,16 @@
},
{
"name": "phpunit/phpunit",
- "version": "8.5.1",
+ "version": "8.5.2",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
- "reference": "7870c78da3c5e4883eaef36ae47853ebb3cb86f2"
+ "reference": "018b6ac3c8ab20916db85fa91bf6465acb64d1e0"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/7870c78da3c5e4883eaef36ae47853ebb3cb86f2",
- "reference": "7870c78da3c5e4883eaef36ae47853ebb3cb86f2",
+ "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/018b6ac3c8ab20916db85fa91bf6465acb64d1e0",
+ "reference": "018b6ac3c8ab20916db85fa91bf6465acb64d1e0",
"shasum": ""
},
"require": {
@@ -2051,7 +2210,7 @@
"testing",
"xunit"
],
- "time": "2019-12-25T14:49:39+00:00"
+ "time": "2020-01-08T08:49:49+00:00"
},
{
"name": "psr/container",
@@ -2103,51 +2262,224 @@
"time": "2017-02-14T16:28:37+00:00"
},
{
- "name": "psr/log",
- "version": "1.1.2",
+ "name": "roave/security-advisories",
+ "version": "dev-master",
"source": {
"type": "git",
- "url": "https://github.com/php-fig/log.git",
- "reference": "446d54b4cb6bf489fc9d75f55843658e6f25d801"
+ "url": "https://github.com/Roave/SecurityAdvisories.git",
+ "reference": "67ac6ea8f4a078c3c9b7aec5d7ae70f098c37389"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/php-fig/log/zipball/446d54b4cb6bf489fc9d75f55843658e6f25d801",
- "reference": "446d54b4cb6bf489fc9d75f55843658e6f25d801",
+ "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/67ac6ea8f4a078c3c9b7aec5d7ae70f098c37389",
+ "reference": "67ac6ea8f4a078c3c9b7aec5d7ae70f098c37389",
"shasum": ""
},
- "require": {
- "php": ">=5.3.0"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "1.1.x-dev"
- }
- },
- "autoload": {
- "psr-4": {
- "Psr\\Log\\": "Psr/Log/"
- }
- },
+ "conflict": {
+ "3f/pygmentize": "<1.2",
+ "adodb/adodb-php": "<5.20.12",
+ "alterphp/easyadmin-extension-bundle": ">=1.2,<1.2.11|>=1.3,<1.3.1",
+ "amphp/artax": "<1.0.6|>=2,<2.0.6",
+ "amphp/http": "<1.0.1",
+ "api-platform/core": ">=2.2,<2.2.10|>=2.3,<2.3.6",
+ "asymmetricrypt/asymmetricrypt": ">=0,<9.9.99",
+ "aws/aws-sdk-php": ">=3,<3.2.1",
+ "brightlocal/phpwhois": "<=4.2.5",
+ "bugsnag/bugsnag-laravel": ">=2,<2.0.2",
+ "cakephp/cakephp": ">=1.3,<1.3.18|>=2,<2.4.99|>=2.5,<2.5.99|>=2.6,<2.6.12|>=2.7,<2.7.6|>=3,<3.5.18|>=3.6,<3.6.15|>=3.7,<3.7.7",
+ "cart2quote/module-quotation": ">=4.1.6,<=4.4.5|>=5,<5.4.4",
+ "cartalyst/sentry": "<=2.1.6",
+ "codeigniter/framework": "<=3.0.6",
+ "composer/composer": "<=1-alpha.11",
+ "contao-components/mediaelement": ">=2.14.2,<2.21.1",
+ "contao/core": ">=2,<3.5.39",
+ "contao/core-bundle": ">=4,<4.4.46|>=4.5,<4.8.6",
+ "contao/listing-bundle": ">=4,<4.4.8",
+ "datadog/dd-trace": ">=0.30,<0.30.2",
+ "david-garcia/phpwhois": "<=4.3.1",
+ "doctrine/annotations": ">=1,<1.2.7",
+ "doctrine/cache": ">=1,<1.3.2|>=1.4,<1.4.2",
+ "doctrine/common": ">=2,<2.4.3|>=2.5,<2.5.1",
+ "doctrine/dbal": ">=2,<2.0.8|>=2.1,<2.1.2",
+ "doctrine/doctrine-bundle": "<1.5.2",
+ "doctrine/doctrine-module": "<=0.7.1",
+ "doctrine/mongodb-odm": ">=1,<1.0.2",
+ "doctrine/mongodb-odm-bundle": ">=2,<3.0.1",
+ "doctrine/orm": ">=2,<2.4.8|>=2.5,<2.5.1",
+ "dompdf/dompdf": ">=0.6,<0.6.2",
+ "drupal/core": ">=7,<7.69|>=8,<8.7.11|>=8.8,<8.8.1",
+ "drupal/drupal": ">=7,<7.69|>=8,<8.7.11|>=8.8,<8.8.1",
+ "endroid/qr-code-bundle": "<3.4.2",
+ "erusev/parsedown": "<1.7.2",
+ "ezsystems/ezplatform-admin-ui": ">=1.3,<1.3.5|>=1.4,<1.4.4",
+ "ezsystems/ezpublish-kernel": ">=5.3,<5.3.12.1|>=5.4,<5.4.13.1|>=6,<6.7.9.1|>=6.8,<6.13.5.1|>=7,<7.2.4.1|>=7.3,<7.3.2.1",
+ "ezsystems/ezpublish-legacy": ">=5.3,<5.3.12.6|>=5.4,<5.4.12.3|>=2011,<2017.12.4.3|>=2018.6,<2018.6.1.4|>=2018.9,<2018.9.1.3",
+ "ezsystems/repository-forms": ">=2.3,<2.3.2.1",
+ "ezyang/htmlpurifier": "<4.1.1",
+ "firebase/php-jwt": "<2",
+ "fooman/tcpdf": "<6.2.22",
+ "fossar/tcpdf-parser": "<6.2.22",
+ "friendsofsymfony/rest-bundle": ">=1.2,<1.2.2",
+ "friendsofsymfony/user-bundle": ">=1.2,<1.3.5",
+ "fuel/core": "<1.8.1",
+ "gree/jose": "<=2.2",
+ "gregwar/rst": "<1.0.3",
+ "guzzlehttp/guzzle": ">=4-rc.2,<4.2.4|>=5,<5.3.1|>=6,<6.2.1",
+ "illuminate/auth": ">=4,<4.0.99|>=4.1,<=4.1.31|>=4.2,<=4.2.22|>=5,<=5.0.35|>=5.1,<=5.1.46|>=5.2,<=5.2.45|>=5.3,<=5.3.31|>=5.4,<=5.4.36|>=5.5,<5.5.10",
+ "illuminate/cookie": ">=4,<=4.0.11|>=4.1,<=4.1.31|>=4.2,<=4.2.22|>=5,<=5.0.35|>=5.1,<=5.1.46|>=5.2,<=5.2.45|>=5.3,<=5.3.31|>=5.4,<=5.4.36|>=5.5,<5.5.42|>=5.6,<5.6.30",
+ "illuminate/database": ">=4,<4.0.99|>=4.1,<4.1.29",
+ "illuminate/encryption": ">=4,<=4.0.11|>=4.1,<=4.1.31|>=4.2,<=4.2.22|>=5,<=5.0.35|>=5.1,<=5.1.46|>=5.2,<=5.2.45|>=5.3,<=5.3.31|>=5.4,<=5.4.36|>=5.5,<5.5.40|>=5.6,<5.6.15",
+ "ivankristianto/phpwhois": "<=4.3",
+ "james-heinrich/getid3": "<1.9.9",
+ "joomla/session": "<1.3.1",
+ "jsmitty12/phpwhois": "<5.1",
+ "kazist/phpwhois": "<=4.2.6",
+ "kreait/firebase-php": ">=3.2,<3.8.1",
+ "la-haute-societe/tcpdf": "<6.2.22",
+ "laravel/framework": ">=4,<4.0.99|>=4.1,<=4.1.31|>=4.2,<=4.2.22|>=5,<=5.0.35|>=5.1,<=5.1.46|>=5.2,<=5.2.45|>=5.3,<=5.3.31|>=5.4,<=5.4.36|>=5.5,<5.5.42|>=5.6,<5.6.30",
+ "laravel/socialite": ">=1,<1.0.99|>=2,<2.0.10",
+ "league/commonmark": "<0.18.3",
+ "magento/magento1ce": "<1.9.4.3",
+ "magento/magento1ee": ">=1,<1.14.4.3",
+ "magento/product-community-edition": ">=2,<2.2.10|>=2.3,<2.3.2-p.2",
+ "monolog/monolog": ">=1.8,<1.12",
+ "namshi/jose": "<2.2",
+ "onelogin/php-saml": "<2.10.4",
+ "openid/php-openid": "<2.3",
+ "oro/crm": ">=1.7,<1.7.4",
+ "oro/platform": ">=1.7,<1.7.4",
+ "padraic/humbug_get_contents": "<1.1.2",
+ "pagarme/pagarme-php": ">=0,<3",
+ "paragonie/random_compat": "<2",
+ "paypal/merchant-sdk-php": "<3.12",
+ "pear/archive_tar": "<1.4.4",
+ "phpmailer/phpmailer": ">=5,<5.2.27|>=6,<6.0.6",
+ "phpoffice/phpexcel": "<=1.8.1",
+ "phpoffice/phpspreadsheet": "<=1.5",
+ "phpunit/phpunit": ">=4.8.19,<4.8.28|>=5.0.10,<5.6.3",
+ "phpwhois/phpwhois": "<=4.2.5",
+ "phpxmlrpc/extras": "<0.6.1",
+ "propel/propel": ">=2-alpha.1,<=2-alpha.7",
+ "propel/propel1": ">=1,<=1.7.1",
+ "pusher/pusher-php-server": "<2.2.1",
+ "robrichards/xmlseclibs": ">=1,<3.0.4",
+ "sabre/dav": ">=1.6,<1.6.99|>=1.7,<1.7.11|>=1.8,<1.8.9",
+ "scheb/two-factor-bundle": ">=0,<3.26|>=4,<4.11",
+ "sensiolabs/connect": "<4.2.3",
+ "serluck/phpwhois": "<=4.2.6",
+ "shopware/shopware": "<5.3.7",
+ "silverstripe/cms": ">=3,<=3.0.11|>=3.1,<3.1.11",
+ "silverstripe/forum": "<=0.6.1|>=0.7,<=0.7.3",
+ "silverstripe/framework": ">=3,<3.6.7|>=3.7,<3.7.3|>=4,<4.4",
+ "silverstripe/graphql": ">=2,<2.0.5|>=3,<3.1.2",
+ "silverstripe/registry": ">=2.1,<2.1.2|>=2.2,<2.2.1",
+ "silverstripe/restfulserver": ">=1,<1.0.9|>=2,<2.0.4",
+ "silverstripe/userforms": "<3",
+ "simple-updates/phpwhois": "<=1",
+ "simplesamlphp/saml2": "<1.10.6|>=2,<2.3.8|>=3,<3.1.4",
+ "simplesamlphp/simplesamlphp": "<1.17.8",
+ "simplesamlphp/simplesamlphp-module-infocard": "<1.0.1",
+ "slim/slim": "<2.6",
+ "smarty/smarty": "<3.1.33",
+ "socalnick/scn-social-auth": "<1.15.2",
+ "spoonity/tcpdf": "<6.2.22",
+ "squizlabs/php_codesniffer": ">=1,<2.8.1|>=3,<3.0.1",
+ "stormpath/sdk": ">=0,<9.9.99",
+ "studio-42/elfinder": "<2.1.48",
+ "swiftmailer/swiftmailer": ">=4,<5.4.5",
+ "sylius/admin-bundle": ">=1,<1.0.17|>=1.1,<1.1.9|>=1.2,<1.2.2",
+ "sylius/grid": ">=1,<1.1.19|>=1.2,<1.2.18|>=1.3,<1.3.13|>=1.4,<1.4.5|>=1.5,<1.5.1",
+ "sylius/grid-bundle": ">=1,<1.1.19|>=1.2,<1.2.18|>=1.3,<1.3.13|>=1.4,<1.4.5|>=1.5,<1.5.1",
+ "sylius/sylius": ">=1,<1.1.18|>=1.2,<1.2.17|>=1.3,<1.3.12|>=1.4,<1.4.4",
+ "symfony/cache": ">=3.1,<3.4.35|>=4,<4.2.12|>=4.3,<4.3.8",
+ "symfony/dependency-injection": ">=2,<2.0.17|>=2.7,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7",
+ "symfony/form": ">=2.3,<2.3.35|>=2.4,<2.6.12|>=2.7,<2.7.50|>=2.8,<2.8.49|>=3,<3.4.20|>=4,<4.0.15|>=4.1,<4.1.9|>=4.2,<4.2.1",
+ "symfony/framework-bundle": ">=2,<2.3.18|>=2.4,<2.4.8|>=2.5,<2.5.2|>=2.7,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7",
+ "symfony/http-foundation": ">=2,<2.8.52|>=3,<3.4.35|>=4,<4.2.12|>=4.3,<4.3.8",
+ "symfony/http-kernel": ">=2,<2.8.52|>=3,<3.4.35|>=4,<4.2.12|>=4.3,<4.3.8",
+ "symfony/intl": ">=2.7,<2.7.38|>=2.8,<2.8.31|>=3,<3.2.14|>=3.3,<3.3.13",
+ "symfony/mime": ">=4.3,<4.3.8",
+ "symfony/phpunit-bridge": ">=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7",
+ "symfony/polyfill": ">=1,<1.10",
+ "symfony/polyfill-php55": ">=1,<1.10",
+ "symfony/proxy-manager-bridge": ">=2.7,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7",
+ "symfony/routing": ">=2,<2.0.19",
+ "symfony/security": ">=2,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7",
+ "symfony/security-bundle": ">=2,<2.7.48|>=2.8,<2.8.41|>=3,<3.3.17|>=3.4,<3.4.11|>=4,<4.0.11",
+ "symfony/security-core": ">=2.4,<2.6.13|>=2.7,<2.7.9|>=2.7.30,<2.7.32|>=2.8,<2.8.37|>=3,<3.3.17|>=3.4,<3.4.7|>=4,<4.0.7",
+ "symfony/security-csrf": ">=2.4,<2.7.48|>=2.8,<2.8.41|>=3,<3.3.17|>=3.4,<3.4.11|>=4,<4.0.11",
+ "symfony/security-guard": ">=2.8,<2.8.41|>=3,<3.3.17|>=3.4,<3.4.11|>=4,<4.0.11",
+ "symfony/security-http": ">=2.3,<2.3.41|>=2.4,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.2.12|>=4.3,<4.3.8",
+ "symfony/serializer": ">=2,<2.0.11",
+ "symfony/symfony": ">=2,<2.8.52|>=3,<3.4.35|>=4,<4.2.12|>=4.3,<4.3.8",
+ "symfony/translation": ">=2,<2.0.17",
+ "symfony/validator": ">=2,<2.0.24|>=2.1,<2.1.12|>=2.2,<2.2.5|>=2.3,<2.3.3",
+ "symfony/var-exporter": ">=4.2,<4.2.12|>=4.3,<4.3.8",
+ "symfony/web-profiler-bundle": ">=2,<2.3.19|>=2.4,<2.4.9|>=2.5,<2.5.4",
+ "symfony/yaml": ">=2,<2.0.22|>=2.1,<2.1.7",
+ "tecnickcom/tcpdf": "<6.2.22",
+ "thelia/backoffice-default-template": ">=2.1,<2.1.2",
+ "thelia/thelia": ">=2.1-beta.1,<2.1.3",
+ "theonedemon/phpwhois": "<=4.2.5",
+ "titon/framework": ">=0,<9.9.99",
+ "truckersmp/phpwhois": "<=4.3.1",
+ "twig/twig": "<1.38|>=2,<2.7",
+ "typo3/cms": ">=6.2,<6.2.30|>=7,<7.6.32|>=8,<8.7.30|>=9,<9.5.12|>=10,<10.2.1",
+ "typo3/cms-core": ">=8,<8.7.30|>=9,<9.5.12|>=10,<10.2.1",
+ "typo3/flow": ">=1,<1.0.4|>=1.1,<1.1.1|>=2,<2.0.1|>=2.3,<2.3.16|>=3,<3.0.10|>=3.1,<3.1.7|>=3.2,<3.2.7|>=3.3,<3.3.5",
+ "typo3/neos": ">=1.1,<1.1.3|>=1.2,<1.2.13|>=2,<2.0.4",
+ "typo3/phar-stream-wrapper": ">=1,<2.1.1|>=3,<3.1.1",
+ "ua-parser/uap-php": "<3.8",
+ "wallabag/tcpdf": "<6.2.22",
+ "willdurand/js-translation-bundle": "<2.1.1",
+ "yiisoft/yii": ">=1.1.14,<1.1.15",
+ "yiisoft/yii2": "<2.0.15",
+ "yiisoft/yii2-bootstrap": "<2.0.4",
+ "yiisoft/yii2-dev": "<2.0.15",
+ "yiisoft/yii2-elasticsearch": "<2.0.5",
+ "yiisoft/yii2-gii": "<2.0.4",
+ "yiisoft/yii2-jui": "<2.0.4",
+ "yiisoft/yii2-redis": "<2.0.8",
+ "zendframework/zend-cache": ">=2.4,<2.4.8|>=2.5,<2.5.3",
+ "zendframework/zend-captcha": ">=2,<2.4.9|>=2.5,<2.5.2",
+ "zendframework/zend-crypt": ">=2,<2.4.9|>=2.5,<2.5.2",
+ "zendframework/zend-db": ">=2,<2.0.99|>=2.1,<2.1.99|>=2.2,<2.2.10|>=2.3,<2.3.5",
+ "zendframework/zend-developer-tools": ">=1.2.2,<1.2.3",
+ "zendframework/zend-diactoros": ">=1,<1.8.4",
+ "zendframework/zend-feed": ">=1,<2.10.3",
+ "zendframework/zend-form": ">=2,<2.2.7|>=2.3,<2.3.1",
+ "zendframework/zend-http": ">=1,<2.8.1",
+ "zendframework/zend-json": ">=2.1,<2.1.6|>=2.2,<2.2.6",
+ "zendframework/zend-ldap": ">=2,<2.0.99|>=2.1,<2.1.99|>=2.2,<2.2.8|>=2.3,<2.3.3",
+ "zendframework/zend-mail": ">=2,<2.4.11|>=2.5,<2.7.2",
+ "zendframework/zend-navigation": ">=2,<2.2.7|>=2.3,<2.3.1",
+ "zendframework/zend-session": ">=2,<2.0.99|>=2.1,<2.1.99|>=2.2,<2.2.9|>=2.3,<2.3.4",
+ "zendframework/zend-validator": ">=2.3,<2.3.6",
+ "zendframework/zend-view": ">=2,<2.2.7|>=2.3,<2.3.1",
+ "zendframework/zend-xmlrpc": ">=2.1,<2.1.6|>=2.2,<2.2.6",
+ "zendframework/zendframework": "<2.5.1",
+ "zendframework/zendframework1": "<1.12.20",
+ "zendframework/zendopenid": ">=2,<2.0.2",
+ "zendframework/zendxml": ">=1,<1.0.1",
+ "zetacomponents/mail": "<1.8.2",
+ "zf-commons/zfc-user": "<1.2.2",
+ "zfcampus/zf-apigility-doctrine": ">=1,<1.0.3",
+ "zfr/zfr-oauth2-server-module": "<0.1.2"
+ },
+ "type": "metapackage",
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
- "name": "PHP-FIG",
- "homepage": "http://www.php-fig.org/"
+ "name": "Marco Pivetta",
+ "email": "ocramius@gmail.com",
+ "role": "maintainer"
}
],
- "description": "Common interface for logging libraries",
- "homepage": "https://github.com/php-fig/log",
- "keywords": [
- "log",
- "psr",
- "psr-3"
- ],
- "time": "2019-11-01T11:05:21+00:00"
+ "description": "Prevents installation of composer packages with known security vulnerabilities: no API, simply require it",
+ "time": "2020-01-06T19:16:46+00:00"
},
{
"name": "sabre/event",
@@ -3274,6 +3606,7 @@
"aliases": [],
"minimum-stability": "stable",
"stability-flags": {
+ "kwi/urllinker": 20,
"pear-pear.horde.org/horde_date": 0,
"pear-pear.horde.org/horde_exception": 0,
"pear-pear.horde.org/horde_imap_client": 0,
@@ -3286,7 +3619,7 @@
"pear-pear.horde.org/horde_text_flowed": 0,
"pear-pear.horde.org/horde_util": 0,
"pear-pear.horde.org/horde_smtp": 0,
- "kwi/urllinker": 20
+ "roave/security-advisories": 20
},
"prefer-stable": false,
"prefer-lowest": false,
diff --git a/lib/Controller/MessagesController.php b/lib/Controller/MessagesController.php
index d4ca2c4a6..22b5d7b03 100755
--- a/lib/Controller/MessagesController.php
+++ b/lib/Controller/MessagesController.php
@@ -31,7 +31,6 @@ declare(strict_types=1);
namespace OCA\Mail\Controller;
use Exception;
-use OCA\Mail\Account;
use OCA\Mail\Contracts\IMailManager;
use OCA\Mail\Contracts\IMailSearch;
use OCA\Mail\Exception\ServiceException;
@@ -40,6 +39,7 @@ use OCA\Mail\Http\HtmlResponse;
use OCA\Mail\Model\IMAPMessage;
use OCA\Mail\Service\AccountService;
use OCA\Mail\Service\IMailBox;
+use OCA\Mail\Service\ItineraryService;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Http;
@@ -47,7 +47,6 @@ use OCP\AppFramework\Http\ContentSecurityPolicy;
use OCP\AppFramework\Http\JSONResponse;
use OCP\AppFramework\Http\Response;
use OCP\AppFramework\Http\TemplateResponse;
-use OCP\AppFramework\Utility\ITimeFactory;
use OCP\Files\Folder;
use OCP\Files\IMimeTypeDetector;
use OCP\IL10N;
@@ -67,6 +66,9 @@ class MessagesController extends Controller {
/** @var IMailSearch */
private $mailSearch;
+ /** @var ItineraryService */
+ private $itineraryService;
+
/** @var string */
private $currentUserId;
@@ -101,6 +103,7 @@ class MessagesController extends Controller {
AccountService $accountService,
IMailManager $mailManager,
IMailSearch $mailSearch,
+ ItineraryService $itineraryService,
string $UserId,
$userFolder,
ILogger $logger,
@@ -112,6 +115,7 @@ class MessagesController extends Controller {
$this->accountService = $accountService;
$this->mailManager = $mailManager;
$this->mailSearch = $mailSearch;
+ $this->itineraryService = $itineraryService;
$this->currentUserId = $UserId;
$this->userFolder = $userFolder;
$this->logger = $logger;
@@ -180,6 +184,11 @@ class MessagesController extends Controller {
base64_decode($folderId),
$id
);
+ $json['itineraries'] = $this->itineraryService->extract(
+ $account,
+ base64_decode($folderId),
+ $id
+ );
$json['attachments'] = array_map(function ($a) use ($accountId, $folderId, $id) {
return $this->enrichDownloadUrl($accountId, $folderId, $id, $a);
}, $json['attachments']);
diff --git a/lib/IMAP/MessageMapper.php b/lib/IMAP/MessageMapper.php
index 93c4376a9..f42f8bc35 100644
--- a/lib/IMAP/MessageMapper.php
+++ b/lib/IMAP/MessageMapper.php
@@ -31,11 +31,13 @@ use Horde_Imap_Client_Fetch_Query;
use Horde_Imap_Client_Ids;
use Horde_Imap_Client_Socket;
use Horde_Mime_Mail;
+use Horde_Mime_Part;
use OCA\Mail\Db\Mailbox;
use OCA\Mail\Exception\ServiceException;
use OCA\Mail\Model\IMAPMessage;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\ILogger;
+use function iterator_to_array;
class MessageMapper {
@@ -214,4 +216,134 @@ class MessageMapper {
);
}
+ public function getHtmlBody(Horde_Imap_Client_Socket $client,
+ string $mailbox,
+ int $id): ?string {
+ $messageQuery = new Horde_Imap_Client_Fetch_Query();
+ $messageQuery->envelope();
+ $messageQuery->structure();
+
+ $result = $client->fetch($mailbox, $messageQuery, [
+ 'ids' => new Horde_Imap_Client_Ids([$id]),
+ ]);
+
+ if (($message = $result->first()) === null) {
+ throw new DoesNotExistException('Message does not exist');
+ }
+
+ $structure = $message->getStructure();
+ $htmlPartId = $structure->findBody('html');
+ if ($htmlPartId === null) {
+ // No HTML part
+ return null;
+ }
+ $partsQuery = new Horde_Imap_Client_Fetch_Query();
+ $partsQuery->fullText();
+ foreach ($structure->partIterator() as $structurePart) {
+ /** @var Horde_Mime_Part $structurePart */
+ $partsQuery->bodyPart($structurePart->getMimeId(), [
+ 'decode' => true,
+ 'peek' => true,
+ ]);
+ $partsQuery->bodyPartSize($structurePart->getMimeId());
+ if ($structurePart->getMimeId() === $htmlPartId) {
+ $partsQuery->mimeHeader($structurePart->getMimeId(), [
+ 'peek' => true
+ ]);
+ }
+
+ }
+
+ $parts = $client->fetch($mailbox, $partsQuery, [
+ 'ids' => new Horde_Imap_Client_Ids([$id]),
+ ]);
+
+ foreach ($parts as $part) {
+ /** @var Horde_Imap_Client_Data_Fetch $part */
+ $stream = $part->getBodyPart($htmlPartId, true);
+ $partData = $structure->getPart($htmlPartId);
+ $partData->setContents($stream, [
+ 'usestream' => true,
+ ]);
+
+ $body = $part->getBodyPart($htmlPartId);
+ if ($body !== null) {
+ $structurePart = $structure[$htmlPartId];
+ $mimeHeaders = $part->getMimeHeader($htmlPartId, Horde_Imap_Client_Data_Fetch::HEADER_PARSE);
+ if ($enc = $mimeHeaders->getValue('content-transfer-encoding')) {
+ $structure->setTransferEncoding($enc);
+ }
+ $structure->setContents($body);
+ $decoded = $structure->getContents();
+
+ return $decoded;
+ }
+ }
+
+ return null;
+ }
+
+ public function getRawAttachments(Horde_Imap_Client_Socket $client,
+ string $mailbox,
+ int $id): array {
+ $messageQuery = new Horde_Imap_Client_Fetch_Query();
+ $messageQuery->structure();
+
+ $result = $client->fetch($mailbox, $messageQuery, [
+ 'ids' => new Horde_Imap_Client_Ids([$id]),
+ ]);
+
+ if (($structureResult = $result->first()) === null) {
+ throw new DoesNotExistException('Message does not exist');
+ }
+
+ $structure = $structureResult->getStructure();
+ $partsQuery = new Horde_Imap_Client_Fetch_Query();
+ $partsQuery->fullText();
+ foreach ($structure->partIterator() as $part) {
+ /** @var Horde_Mime_Part $part */
+ if ($part->getMimeId() === "0") {
+ // Ignore message header
+ continue;
+ }
+
+ $partsQuery->bodyPart($part->getMimeId(), [
+ 'peek' => true,
+ ]);
+ $partsQuery->mimeHeader($part->getMimeId(), [
+ 'peek' => true
+ ]);
+ $partsQuery->bodyPartSize($part->getMimeId());
+ }
+
+ $parts = $client->fetch($mailbox, $partsQuery, [
+ 'ids' => new Horde_Imap_Client_Ids([$id]),
+ ]);
+ if (($messageData = $parts->first()) === null) {
+ throw new DoesNotExistException('Message does not exist');
+ }
+
+ $attachments = [];
+ foreach ($structure->partIterator() as $key => $part) {
+ /** @var Horde_Mime_Part $part */
+
+ if (!$part->isAttachment()) {
+ continue;
+ }
+
+ $stream = $messageData->getBodyPart($key, true);
+ $mimeHeaders = $messageData->getMimeHeader($key, Horde_Imap_Client_Data_Fetch::HEADER_PARSE);
+ if ($enc = $mimeHeaders->getValue('content-transfer-encoding')) {
+ $part->setTransferEncoding($enc);
+ }
+ $part->setContents($stream, [
+ 'usestream' => true,
+ ]);
+ $decoded = $part->getContents();
+
+ $attachments[] = $decoded;
+ }
+ return $attachments;
+ }
+
}
diff --git a/lib/Integration/KItinerary/ItineraryExtractor.php b/lib/Integration/KItinerary/ItineraryExtractor.php
new file mode 100644
index 000000000..9e6e6e932
--- /dev/null
+++ b/lib/Integration/KItinerary/ItineraryExtractor.php
@@ -0,0 +1,88 @@
+<?php declare(strict_types=1);
+
+/**
+ * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+namespace OCA\Mail\Integration\KItinerary;
+
+use ChristophWurst\KItinerary\Adapter;
+use ChristophWurst\KItinerary\Exception\KItineraryRuntimeException;
+use ChristophWurst\KItinerary\Flatpak\FlatpakAdapter;
+use ChristophWurst\KItinerary\Itinerary;
+use ChristophWurst\KItinerary\ItineraryExtractor as Extractor;
+use ChristophWurst\KItinerary\Bin\BinaryAdapter;
+use OCA\Mail\Integration\Psr\LoggerAdapter;
+use OCP\ILogger;
+
+class ItineraryExtractor {
+
+ /** @var BinaryAdapter */
+ private $binAdapter;
+
+ /** @var FlatpakAdapter */
+ private $flatpakAdapter;
+
+ /** @var ILogger */
+ private $logger;
+
+ /** @var Adapter */
+ private $adapter;
+
+ public function __construct(BinaryAdapter $binAdapter,
+ FlatpakAdapter $flatpakAdapter,
+ ILogger $logger) {
+ $this->binAdapter = $binAdapter;
+ $this->flatpakAdapter = $flatpakAdapter;
+ $this->logger = $logger;
+ }
+
+ private function findAvailableAdapter(): ?Adapter {
+ if ($this->binAdapter->isAvailable()) {
+ $this->binAdapter->setLogger(new LoggerAdapter($this->logger));
+ return $this->binAdapter;
+ }
+ if ($this->flatpakAdapter->isAvailable()) {
+ return $this->flatpakAdapter;
+ }
+ return null;
+ }
+
+ public function extract(string $content): Itinerary {
+ if ($this->adapter === null) {
+ $this->adapter = $this->findAvailableAdapter() ?? false;
+ }
+ if ($this->adapter === false) {
+ $this->logger->warning('KItinerary binary adapter is not available, can\'t extract information');
+
+ return new Itinerary();
+ }
+
+ try {
+ return (new Extractor($this->adapter))->extractFromString($content);
+ } catch (KItineraryRuntimeException $e) {
+ $this->logger->logException($e, [
+ 'message' => 'Could not extract itinerary function from KItinerary integration',
+ ]);
+ return new Itinerary();
+ }
+ }
+
+}
diff --git a/lib/Integration/Psr/LoggerAdapter.php b/lib/Integration/Psr/LoggerAdapter.php
new file mode 100644
index 000000000..34ad54558
--- /dev/null
+++ b/lib/Integration/Psr/LoggerAdapter.php
@@ -0,0 +1,158 @@
+<?php declare(strict_types=1);
+
+/**
+ * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+namespace OCA\Mail\Integration\Psr;
+
+use OCP\ILogger;
+use Psr\Log\LoggerInterface;
+
+class LoggerAdapter implements LoggerInterface {
+
+ /** @var ILogger */
+ private $logger;
+
+ public function __construct(ILogger $logger) {
+ $this->logger = $logger;
+ }
+
+ /**
+ * System is unusable.
+ *
+ * @param string $message
+ * @param array $context
+ *
+ * @return void
+ */
+ public function emergency($message, array $context = array()) {
+ $this->logger->emergency($message, $context);
+ }
+
+ /**
+ * Action must be taken immediately.
+ *
+ * Example: Entire website down, database unavailable, etc. This should
+ * trigger the SMS alerts and wake you up.
+ *
+ * @param string $message
+ * @param array $context
+ *
+ * @return void
+ */
+ public function alert($message, array $context = array()) {
+ $this->logger->alert($message, $context);
+ }
+
+ /**
+ * Critical conditions.
+ *
+ * Example: Application component unavailable, unexpected exception.
+ *
+ * @param string $message
+ * @param array $context
+ *
+ * @return void
+ */
+ public function critical($message, array $context = array()) {
+ $this->logger->critical($message, $context);
+ }
+
+ /**
+ * Runtime errors that do not require immediate action but should typically
+ * be logged and monitored.
+ *
+ * @param string $message
+ * @param array $context
+ *
+ * @return void
+ */
+ public function error($message, array $context = array()) {
+ $this->logger->error($message, $context);
+ }
+
+ /**
+ * Exceptional occurrences that are not errors.
+ *
+ * Example: Use of deprecated APIs, poor use of an API, undesirable things
+ * that are not necessarily wrong.
+ *
+ * @param string $message
+ * @param array $context
+ *
+ * @return void
+ */
+ public function warning($message, array $context = array()) {
+ $this->logger->warning($message, $context);
+ }
+
+ /**
+ * Normal but significant events.
+ *
+ * @param string $message
+ * @param array $context
+ *
+ * @return void
+ */
+ public function notice($message, array $context = array()) {
+ $this->logger->notice($message, $context);
+ }
+
+ /**
+ * Interesting events.
+ *
+ * Example: User logs in, SQL logs.
+ *
+ * @param string $message
+ * @param array $context
+ *
+ * @return void
+ */
+ public function info($message, array $context = array()) {
+ $this->logger->info($message, $context);
+ }
+
+ /**
+ * Detailed debug information.
+ *
+ * @param string $message
+ * @param array $context
+ *
+ * @return void
+ */
+ public function debug($message, array $context = array()) {
+ $this->logger->debug($message, $context);
+ }
+
+ /**
+ * Logs with an arbitrary level.
+ *
+ * @param mixed $level
+ * @param string $message
+ * @param array $context
+ *
+ * @return void
+ */
+ public function log($level, $message, array $context = array()) {
+ $this->logger->log($level, $message, $context);
+ }
+
+}
diff --git a/lib/Model/IMAPMessage.php b/lib/Model/IMAPMessage.php
index 74ec3aa04..c37451ba1 100644
--- a/lib/Model/IMAPMessage.php
+++ b/lib/Model/IMAPMessage.php
@@ -29,7 +29,6 @@ declare(strict_types=1);
namespace OCA\Mail\Model;
-use Closure;
use Exception;
use Horde_Imap_Client;
use Horde_Imap_Client_Data_Envelope;
@@ -48,7 +47,6 @@ use OCA\Mail\Service\Html;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\Files\File;
use OCP\Files\SimpleFS\ISimpleFile;
-use OCP\Util;
use function base64_encode;
use function mb_convert_encoding;
@@ -413,6 +411,7 @@ class IMAPMessage implements IMessage, JsonSerializable {
public function jsonSerialize(): array {
return [
'id' => $this->getUid(),
+ 'messageId' => $this->getMessageId(),
'from' => $this->getFrom()->jsonSerialize(),
'to' => $this->getTo()->jsonSerialize(),
'cc' => $this->getCC()->jsonSerialize(),
diff --git a/lib/Service/ItineraryService.php b/lib/Service/ItineraryService.php
new file mode 100644
index 000000000..a318bf4e4
--- /dev/null
+++ b/lib/Service/ItineraryService.php
@@ -0,0 +1,106 @@
+<?php declare(strict_types=1);
+
+/**
+ * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+namespace OCA\Mail\Service;
+
+use ChristophWurst\KItinerary\Itinerary;
+use OCA\Mail\Account;
+use OCA\Mail\Db\MailboxMapper;
+use OCA\Mail\IMAP\IMAPClientFactory;
+use OCA\Mail\IMAP\MessageMapper;
+use OCA\Mail\Integration\KItinerary\ItineraryExtractor;
+use OCP\ICacheFactory;
+use OCP\ILogger;
+use function array_reduce;
+use function count;
+use function json_encode;
+
+class ItineraryService {
+
+ /** @var IMAPClientFactory */
+ private $clientFactory;
+
+ /** @var MailboxMapper */
+ private $mailboxMapper;
+
+ /** @var MessageMapper */
+ private $messageMapper;
+
+ /** @var ItineraryExtractor */
+ private $extractor;
+
+ /** @var ILogger */
+ private $logger;
+
+ public function __construct(IMAPClientFactory $clientFactory,
+ MailboxMapper $mailboxMapper,
+ MessageMapper $messageMapper,
+ ItineraryExtractor $extractor,
+ ICacheFactory $cacheFactory,
+ ILogger $logger) {
+ $this->clientFactory = $clientFactory;
+ $this->mailboxMapper = $mailboxMapper;
+ $this->messageMapper = $messageMapper;
+ $this->extractor = $extractor;
+ $this->cache = $cacheFactory->createLocal();
+ $this->logger = $logger;
+ }
+
+ public function extract(Account $account, string $mailbox, int $id): Itinerary {
+ $mailbox = $this->mailboxMapper->find($account, $mailbox);
+
+ $cacheKey = 'mail_itinerary_' . $account->getId() . '_' . $mailbox->getMailbox() . '_' . $id;
+ if ($cached = ($this->cache->get($cacheKey))) {
+ return Itinerary::fromJson($cached);
+ }
+
+ $client = $this->clientFactory->getClient($account);
+
+ $itinerary = new Itinerary();
+ $htmlBody = $this->messageMapper->getHtmlBody($client, $mailbox->getMailbox(), $id);
+ if ($htmlBody !== null) {
+ $itinerary = $itinerary->merge(
+ $this->extractor->extract($htmlBody)
+ );
+ $this->logger->debug('Extracted ' . count($itinerary) . ' itinerary entries from the message HTML body');
+ } else {
+ $this->logger->debug('Message does not have an HTML body, can\'t extract itinerary info');
+ }
+ $attachments = $this->messageMapper->getRawAttachments($client, $mailbox->getMailbox(), $id);
+ $itinerary = array_reduce($attachments, function(Itinerary $combined, string $attachment) {
+ $extracted = $this->extractor->extract($attachment);
+ $this->logger->debug('Extracted ' . count($extracted) . ' itinerary entries from an attachment');
+ return $combined->merge($extracted);
+ }, $itinerary);
+
+ // Lastly, we put the extracted data through the tool again, so it can combine
+ // and pick the most relevant information
+ $final = $this->extractor->extract(json_encode($itinerary));
+ $this->logger->debug('Reduced ' . count($itinerary) . ' itinerary entries to ' . count($final) . ' entries');
+
+ $this->cache->set($cacheKey, json_encode($final));
+
+ return $final;
+ }
+
+}
diff --git a/package-lock.json b/package-lock.json
index 07bb306bd..ae76deeec 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -171,7 +171,7 @@
},
"minimist": {
"version": "1.2.0",
- "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
+ "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ="
}
}
@@ -4549,7 +4549,7 @@
},
"json5": {
"version": "1.0.1",
- "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
+ "resolved": "http://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
"dev": true,
"requires": {
@@ -4569,7 +4569,7 @@
},
"minimist": {
"version": "1.2.0",
- "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
+ "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
"dev": true
},
@@ -7101,7 +7101,7 @@
"dependencies": {
"minimist": {
"version": "1.2.0",
- "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
+ "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ="
}
}
@@ -7121,7 +7121,7 @@
"dependencies": {
"readable-stream": {
"version": "3.3.0",
- "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.3.0.tgz",
+ "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-3.3.0.tgz",
"integrity": "sha512-EsI+s3k3XsW+fU8fQACLN59ky34AZ14LoeVZpYwmZvldCFo0r0gnelwF2TcMjLor/BTL5aDJVBMkss0dthToPw==",
"requires": {
"inherits": "^2.0.3",
@@ -7813,7 +7813,7 @@
},
"json5": {
"version": "0.5.1",
- "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz",
+ "resolved": "http://registry.npmjs.org/json5/-/json5-0.5.1.tgz",
"integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE="
},
"jsonfile": {
@@ -8149,7 +8149,7 @@
"dependencies": {
"minimist": {
"version": "1.2.0",
- "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
+ "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
"dev": true
}
@@ -8268,7 +8268,7 @@
},
"minimist": {
"version": "0.0.8",
- "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
+ "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
"integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0="
},
"mississippi": {
@@ -10652,7 +10652,7 @@
},
"readable-stream": {
"version": "2.3.6",
- "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
+ "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
"integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
"requires": {
"core-util-is": "~1.0.0",
@@ -11068,7 +11068,7 @@
},
"yargs": {
"version": "7.1.0",
- "resolved": "https://registry.npmjs.org/yargs/-/yargs-7.1.0.tgz",
+ "resolved": "http://registry.npmjs.org/yargs/-/yargs-7.1.0.tgz",
"integrity": "sha1-a6MY6xaWFyf10oT46gA+jWFU0Mg=",
"dev": true,
"requires": {
@@ -11693,7 +11693,7 @@
},
"strip-ansi": {
"version": "4.0.0",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
+ "resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
"integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
"requires": {
"ansi-regex": "^3.0.0"
@@ -11711,7 +11711,7 @@
},
"strip-ansi": {
"version": "3.0.1",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
+ "resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
"integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
"requires": {
"ansi-regex": "^2.0.0"
@@ -12603,6 +12603,11 @@
"vue-style-loader": "^4.1.0"
}
},
+ "vue-material-design-icons": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/vue-material-design-icons/-/vue-material-design-icons-4.3.0.tgz",
+ "integrity": "sha512-ohKKNFBFSf31VDy+/xuEleP0IMGeAvR4IA7V7b5ak//TOzgvQFwFXu+uJGvkvNoldlXYdGy5X3+4Qil8ozf8JA=="
+ },
"vue-multiselect": {
"version": "2.1.6",
"resolved": "https://registry.npmjs.org/vue-multiselect/-/vue-multiselect-2.1.6.tgz",
@@ -12845,7 +12850,7 @@
},
"json5": {
"version": "1.0.1",
- "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
+ "resolved": "http://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
"dev": true,
"requires": {
@@ -12865,7 +12870,7 @@
},
"minimist": {
"version": "1.2.0",
- "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
+ "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
"dev": true
},
@@ -12969,7 +12974,7 @@
},
"json5": {
"version": "1.0.1",
- "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
+ "resolved": "http://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
"dev": true,
"requires": {
@@ -12999,7 +13004,7 @@
},
"minimist": {
"version": "1.2.0",
- "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
+ "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
"dev": true
},
@@ -13081,7 +13086,7 @@
},
"yargs": {
"version": "13.2.4",
- "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.2.4.tgz",
+ "resolved": "http://registry.npmjs.org/yargs/-/yargs-13.2.4.tgz",
"integrity": "sha512-HG/DWAJa1PAnHT9JAhNa8AbAv3FPaiLzioSjCcmuXXhP8MlpHO5vwls4g4j6n30Z74GVQj8Xa62dWVx1QCGklg==",
"dev": true,
"requires": {
@@ -13206,7 +13211,7 @@
},
"wrap-ansi": {
"version": "2.1.0",
- "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz",
+ "resolved": "http://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz",
"integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=",
"dev": true,
"requires": {
@@ -13401,7 +13406,7 @@
},
"wrap-ansi": {
"version": "5.1.0",
- "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz",
+ "resolved": "http://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz",
"integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==",
"dev": true,
"requires": {
diff --git a/package.json b/package.json
index 6cb2e2816..cb4c74199 100644
--- a/package.json
+++ b/package.json
@@ -56,6 +56,7 @@
"vue-autosize": "^1.0.2",
"vue-click-outside": "^1.0.7",
"vue-infinite-scroll": "^2.0.2",
+ "vue-material-design-icons": "^4.3.0",
"vue-on-click-outside": "^1.0.3",
"vue-router": "^3.1.3",
"vue-scroll": "^2.1.13",
diff --git a/src/components/Itinerary.vue b/src/components/Itinerary.vue
new file mode 100644
index 000000000..ee4a76124
--- /dev/null
+++ b/src/components/Itinerary.vue
@@ -0,0 +1,71 @@
+<template>
+ <div>
+ <template v-for="(entry, idx) in entries">
+ <EventReservation
+ v-if="entry['@type'] === 'EventReservation'"
+ :key="idx"
+ :data="entry"
+ :calendars="calendars"
+ :message-id="messageId"
+ />
+ <FlightReservation
+ v-else-if="entry['@type'] === 'FlightReservation'"
+ :key="idx"
+ :data="entry"
+ :calendars="calendars"
+ :message-id="messageId"
+ />
+ <TrainReservation
+ v-else-if="entry['@type'] === 'TrainReservation'"
+ :key="idx"
+ :data="entry"
+ :calendars="calendars"
+ :message-id="messageId"
+ />
+ <span v-else :key="idx">{{
+ t('mail', 'Itinerary for {type} is not supported yet', {type: entry['@type']})
+ }}</span>
+ </template>
+ </div>
+</template>
+
+<script>
+import once from 'lodash/fp/once'
+
+import {getUserCalendars} from '../service/DAVService'
+import logger from '../logger'
+import EventReservation from './itinerary/EventReservation'
+import FlightReservation from './itinerary/FlightReservation'
+import TrainReservation from './itinerary/TrainReservation'
+
+const getUserCalendarsOnce = once(getUserCalendars)
+
+export default {
+ name: 'Itinerary',
+ components: {
+ EventReservation,
+ FlightReservation,
+ TrainReservation,
+ },
+ props: {
+ entries: {
+ type: Array,
+ required: true,
+ },
+ messageId: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ calendars: [],
+ }
+ },
+ mounted() {
+ getUserCalendarsOnce()
+ .then(calendars => (this.calendars = calendars))
+ .catch(error => logger.error('Could not load calendars', {error}))
+ },
+}
+</script>
diff --git a/src/components/Message.vue b/src/components/Message.vue
index f89ed6790..886d31fcf 100644
--- a/src/components/Message.vue
+++ b/src/components/Message.vue
@@ -53,6 +53,9 @@
</div>
</div>
<div class="mail-message-body">
+ <div v-if="message.itineraries.length > 0" class="message-itinerary">
+ <Itinerary :entries="message.itineraries" :message-id="message.messageId" />
+ </div>
<MessageHTMLBody v-if="message.hasHtmlBody" :url="htmlUrl" />
<MessagePlainTextBody v-else :body="message.body" :signature="message.signature" />
<MessageAttachments :attachments="message.attachments" />
@@ -72,6 +75,7 @@ import AddressList from './AddressList'
import {buildRecipients as buildReplyRecipients, buildReplySubject} from '../ReplyBuilder'
import Error from './Error'
import {getRandomMessageErrorMessage} from '../util/ErrorMessageFactory'
+import Itinerary from './Itinerary'
import MessageHTMLBody from './MessageHTMLBody'
import MessagePlainTextBody from './MessagePlainTextBody'
import Loading from './Loading'
@@ -86,6 +90,7 @@ export default {
AddressList,
AppContentDetails,
Error,
+ Itinerary,
Loading,
MessageAttachments,
MessageHTMLBody,
@@ -316,7 +321,7 @@ export default {
#mail-content,
.mail-message-attachments {
- margin: 10px 10px 50px 38px;
+ margin: 10px 38px 50px 38px;
}
.mail-message-attachments {
@@ -344,7 +349,7 @@ export default {
flex-direction: row;
justify-content: flex-end;
margin-left: 10px;
- margin-right: 35px;
+ margin-right: 22px;
height: 44px;
}
diff --git a/src/components/itinerary/CalendarImport.vue b/src/components/itinerary/CalendarImport.vue
new file mode 100644
index 000000000..7cbe052c4
--- /dev/null
+++ b/src/components/itinerary/CalendarImport.vue
@@ -0,0 +1,76 @@
+<!--
+ - @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ -
+ - @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ -
+ - @license GNU AGPL version 3 or any later version
+ -
+ - This program is free software: you can redistribute it and/or modify
+ - it under the terms of the GNU Affero General Public License as
+ - published by the Free Software Foundation, either version 3 of the
+ - License, or (at your option) any later version.
+ -
+ - This program is distributed in the hope that it will be useful,
+ - but WITHOUT ANY WARRANTY; without even the implied warranty of
+ - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ - GNU Affero General Public License for more details.
+ -
+ - You should have received a copy of the GNU Affero General Public License
+ - along with this program. If not, see <http://www.gnu.org/licenses/>.
+ -->
+
+<template>
+ <Actions v-if="calendars.length" default-icon="icon-add">
+ <ActionButton
+ v-for="(calendar, idx) in cals"
+ :key="idx"
+ :icon="calendar.loading ? 'icon-loading-small' : 'icon-add'"
+ @click="onImport(calendar)"
+ >{{ t('mail', 'Import into {calendar}', {calendar: calendar.displayname}) }}</ActionButton
+ >
+ </Actions>
+</template>
+
+<script>
+import Actions from '@nextcloud/vue/dist/Components/Actions'
+import ActionButton from '@nextcloud/vue/dist/Components/ActionButton'
+
+export default {
+ name: 'CalendarImport',
+ components: {
+ Actions,
+ ActionButton,
+ },
+ props: {
+ calendars: {
+ type: Array,
+ required: true,
+ },
+ handler: {
+ type: Function,
+ required: true,
+ },
+ },
+ computed: {
+ cals() {
+ return this.calendars.map(original => {
+ this.$set(original, 'loading', false)
+ return original
+ })
+ },
+ },
+ methods: {
+ onImport(calendar) {
+ calendar.loading = true
+
+ this.handler(calendar)
+ .catch(console.error.bind(this))
+ .then(() => {
+ calendar.loading = false
+ })
+ },
+ },
+}
+</script>
+
+<style scoped></style>
diff --git a/src/components/itinerary/EventReservation.vue b/src/components/itinerary/EventReservation.vue
new file mode 100644
index 000000000..8de523491
--- /dev/null
+++ b/src/components/itinerary/EventReservation.vue
@@ -0,0 +1,162 @@
+<!--
+ - @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ -
+ - @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ -
+ - @license GNU AGPL version 3 or any later version
+ -
+ - This program is free software: you can redistribute it and/or modify
+ - it under the terms of the GNU Affero General Public License as
+ - published by the Free Software Foundation, either version 3 of the
+ - License, or (at your option) any later version.
+ -
+ - This program is distributed in the hope that it will be useful,
+ - but WITHOUT ANY WARRANTY; without even the implied warranty of
+ - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ - GNU Affero General Public License for more details.
+ -
+ - You should have received a copy of the GNU Affero General Public License
+ - along with this program. If not, see <http://www.gnu.org/licenses/>.
+ -->
+
+<template>
+ <div class="reservation">
+ <div class="event">
+ <div class="event-name">{{ eventName }}</div>
+ <div v-if="location" class="venue">{{ location }}</div>
+ <div v-if="date">{{ date }}</div>
+ <div v-if="time">{{ time }}</div>
+ </div>
+ <CalendarImport v-if="canImport" :calendars="calendars" :handler="handleImport" />
+ </div>
+</template>
+
+<script>
+import ical from 'ical.js'
+import md5 from 'md5'
+import moment from '@nextcloud/moment'
+
+import CalendarImport from './CalendarImport'
+import {importCalendarEvent, importSingleCalendarEvent} from '../../service/DAVService'
+import logger from '../../logger'
+
+export default {
+ name: 'EventReservation',
+ components: {CalendarImport},
+ props: {
+ data: {
+ type: Object,
+ required: true,
+ },
+ calendars: {
+ type: Array,
+ required: true,
+ },
+ messageId: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ eventName() {
+ return this.data.reservationFor.name
+ },
+ time() {
+ if (!('startDate' in this.data.reservationFor)) {
+ return
+ }
+ return moment(this.data.reservationFor.startDate).format('LT')
+ },
+ date() {
+ if (!('startDate' in this.data.reservationFor)) {
+ return
+ }
+ return moment(this.data.reservationFor.startDate).format('L')
+ },
+ location() {
+ if (!('location' in this.data.reservationFor) || !('name' in this.data.reservationFor.location)) {
+ return
+ }
+ return this.data.reservationFor.location.name
+ },
+ canImport() {
+ return 'startDate' in this.data.reservationFor
+ },
+ },
+ methods: {
+ getEndDateTime(event) {
+ if ('endDate' in this.data.reservationFor) {
+ return moment(this.data.reservationFor.endDate).format()
+ } else {
+ // Assume it's 2h and user will adjust if necessary
+ // TODO: handle 'duration' https://schema.org/Event
+ return moment('2019-10-22T12:00:00Z')
+ .add(2, 'hours')
+ .format()
+ }
+ },
+ handleImport(calendar) {
+ const event = new ical.Component('VEVENT')
+ event.updatePropertyWithValue('SUMMARY', this.eventName)
+
+ const start = moment(this.data.reservationFor.startDate).format()
+ event.updatePropertyWithValue('DTSTART', ical.Time.fromDateTimeString(start))
+ event.updatePropertyWithValue(
+ 'DTEND',
+ ical.Time.fromDateTimeString(this.getEndDateTime(this.data.reservationFor))
+ )
+
+ if ('location' in this.data.reservationFor) {
+ event.updatePropertyWithValue('LOCATION', this.data.reservationFor.location.name)
+ if ('geo' in this.data.reservationFor.location) {
+ // https://www.kanzaki.com/docs/ical/geo.html
+ event.updatePropertyWithValue(
+ 'GEO',
+ `${this.data.reservationFor.location.geo.latitude};${this.data.reservationFor.location.geo.longitude}`
+ )
+ }
+ }
+
+ // TODO: read version from package.json
+ event.updatePropertyWithValue('PRODID', 'Nextcloud Mail')
+
+ // TODO: is this free of collisions? the bug reports will tell us!
+ event.updatePropertyWithValue('UID', md5(this.messageId + this.eventName))
+
+ const cal = new ical.Component('VCALENDAR')
+ cal.addSubcomponent(event)
+ logger.debug('generated calendar event from event reservation data', {ical: cal.toString()})
+ return importCalendarEvent(calendar.url)(cal.toString())
+ .then(() => {
+ logger.debug('event successfully imported')
+ OCP.Toast.success(t('mail', 'Event imported into {calendar}', {calendar: calendar.displayname}))
+ })
+ .catch(error => {
+ logger.error('Could not import event', {error})
+ OCP.Toast.error(t('mail', 'Could not create event'))
+ })
+ },
+ },
+}
+</script>
+
+<style scoped>
+.reservation {
+ display: flex;
+ flex-direction: row;
+ margin: 30px 38px;
+ border: 1px solid var(--color-border);
+ border-radius: var(--border-radius);
+ padding: 20px;
+ align-items: center;
+}
+
+.event {
+ flex-grow: 1;
+}
+
+.event-name {
+ font-size: larger;
+ font-weight: bold;
+}
+</style>
diff --git a/src/components/itinerary/FlightReservation.vue b/src/components/itinerary/FlightReservation.vue
new file mode 100644
index 000000000..7d903b95b
--- /dev/null
+++ b/src/components/itinerary/FlightReservation.vue
@@ -0,0 +1,177 @@
+<template>
+ <div class="reservation">
+ <div class="departure">
+ <div class="iata">{{ data.reservationFor.departureAirport.iataCode }}</div>
+ <div class="airport">{{ data.reservationFor.departureAirport.name }}</div>
+ <div v-if="departureDate">{{ departureDate }}</div>
+ <div v-if="departureTime">{{ departureTime }}</div>
+ </div>
+ <div class="connection">
+ <div><AirplaneIcon :title="t('mail', 'Airplane')" /></div>
+ <div>{{ flightNumber }}</div>
+ <div v-if="reservation">{{ t('mail', 'Reservation {id}', {id: reservation}) }}</div>
+ <div v-else><ArrowIcon decorative /></div>
+ </div>
+ <div class="arrival">
+ <div class="iata">{{ data.reservationFor.arrivalAirport.iataCode }}</div>
+ <div class="airport">{{ data.reservationFor.arrivalAirport.name }}</div>
+ <div v-if="arrivalDate">{{ arrivalDate }}</div>
+ <div v-if="arrivalTime">{{ arrivalTime }}</div>
+ </div>
+ <CalendarImport :calendars="calendars" :handler="handleImport" />
+ </div>
+</template>
+
+<script>
+import AirplaneIcon from 'vue-material-design-icons/Airplane'
+import ArrowIcon from 'vue-material-design-icons/ArrowRight'
+import ical from 'ical.js'
+import md5 from 'md5'
+import moment from '@nextcloud/moment'
+
+import CalendarImport from './CalendarImport'
+import {importCalendarEvent} from '../../service/DAVService'
+import logger from '../../logger'
+
+export default {
+ name: 'FlightReservation',
+ components: {
+ AirplaneIcon,
+ ArrowIcon,
+ CalendarImport,
+ },
+ props: {
+ data: {
+ type: Object,
+ required: true,
+ },
+ calendars: {
+ type: Array,
+ required: true,
+ },
+ messageId: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ departureTime() {
+ if (!('departureTime' in this.data.reservationFor)) {
+ return
+ }
+ return moment(this.data.reservationFor.departureTime['@value']).format('LT')
+ },
+ departureDate() {
+ if (!('departureTime' in this.data.reservationFor)) {
+ return
+ }
+ return moment(this.data.reservationFor.departureTime['@value']).format('L')
+ },
+ arrivalTime() {
+ if (!('arrivalTime' in this.data.reservationFor)) {
+ return
+ }
+ return moment(this.data.reservationFor.arrivalTime['@value']).format('LT')
+ },
+ arrivalDate() {
+ if (!('arrivalTime' in this.data.reservationFor)) {
+ return
+ }
+ return moment(this.data.reservationFor.arrivalTime['@value']).format('L')
+ },
+ flightNumber() {
+ return this.data.reservationFor.airline.iataCode + this.data.reservationFor.flightNumber
+ },
+ reservation() {
+ if (!('reservationNumber' in this.data)) {
+ return
+ }
+ return this.data.reservationNumber
+ },
+ canImport() {
+ return 'departureTime' in this.data.reservationFor && 'arrivalTime' in this.data.reservationFor
+ },
+ },
+ methods: {
+ handleImport(calendar) {
+ const event = new ical.Component('VEVENT')
+ event.updatePropertyWithValue(
+ 'SUMMARY',
+ t('mail', 'Flight {flightNr} from {depAirport} to {arrAirport}', {
+ flightNr: this.flightNumber,
+ depAirport: this.data.reservationFor.departureAirport.iataCode,
+ arrAirport: this.data.reservationFor.arrivalAirport.iataCode,
+ })
+ )
+
+ const depart = moment(this.data.reservationFor.departureTime['@value']).format()
+ event.updatePropertyWithValue('DTSTART', ical.Time.fromDateTimeString(depart))
+ const arrive = moment(this.data.reservationFor.arrivalTime['@value']).format()
+ event.updatePropertyWithValue('DTEND', ical.Time.fromDateTimeString(arrive))
+
+ // TODO: read version from package.json
+ event.updatePropertyWithValue('PRODID', 'Nextcloud Mail')
+
+ // TODO: is this free of collisions? the bug reports will tell us!
+ event.updatePropertyWithValue('UID', md5(this.messageId + this.flightNumber))
+
+ const cal = new ical.Component('VCALENDAR')
+ cal.addSubcomponent(event)
+ logger.debug('generated calendar event from flight reservation data', {ical: cal.toString()})
+
+ return importCalendarEvent(calendar.url)(cal.toString())
+ .then(() => {
+ logger.debug('event successfully imported')
+ OCP.Toast.success(t('mail', 'Event imported into {calendar}', {calendar: calendar.displayname}))
+ })
+ .catch(error => {
+ logger.error('Could not import event', {error})
+ OCP.Toast.error(t('mail', 'Could not create event'))
+ })
+ },
+ },
+}
+</script>
+
+<style scoped>
+.reservation {
+ display: flex;
+ flex-direction: row;
+ margin: 30px 38px;
+ border: 1px solid var(--color-border);
+ border-radius: var(--border-radius);
+ padding: 20px;
+ align-items: center;
+}
+
+.departure,
+.arrival {
+ display: flex;
+ flex-direction: column;
+ flex-grow: 1;
+}
+
+.departure,
+.arrival,
+.connection {
+ justify-content: center;
+}
+
+.iata {
+ font-size: larger;
+ font-weight: bold;
+}
+
+.airport {
+ font-size: large;
+}
+
+.departure {
+ text-align: right;
+}
+
+.connection {
+ text-align: center;
+ padding: 0 40px;
+}
+</style>
diff --git a/src/components/itinerary/TrainReservation.vue b/src/components/itinerary/TrainReservation.vue
new file mode 100644
index 000000000..b16f1fbe4
--- /dev/null
+++ b/src/components/itinerary/TrainReservation.vue
@@ -0,0 +1,185 @@
+<template>
+ <div class="reservation">
+ <div class="departure">
+ <div class="station">{{ data.reservationFor.departureStation.name }}</div>
+ <div v-if="departureDate">{{ departureDate }}</div>
+ <div v-if="departureTime">{{ departureTime }}</div>
+ </div>
+ <div class="connection">
+ <div><TrainIcon :title="t('mail', 'Tain')" /></div>
+ <div>{{ trainNumber }}</div>
+ <div><ArrowIcon decorative /></div>
+ </div>
+ <div class="arrival">
+ <div class="station">{{ data.reservationFor.arrivalStation.name }}</div>
+ <div v-if="arrivalDate">{{ arrivalDate }}</div>
+ <div v-if="arrivalTime">{{ arrivalTime }}</div>
+ </div>
+ <CalendarImport v-if="canImport" :calendars="calendars" :handler="handleImport" />
+ </div>
+</template>
+
+<script>
+import ArrowIcon from 'vue-material-design-icons/ArrowRight'
+import ical from 'ical.js'
+import md5 from 'md5'
+import moment from '@nextcloud/moment'
+import TrainIcon from 'vue-material-design-icons/Train'
+
+import CalendarImport from './CalendarImport'
+import {importCalendarEvent} from '../../service/DAVService'
+import logger from '../../logger'
+
+export default {
+ name: 'TrainReservation',
+ components: {
+ ArrowIcon,
+ CalendarImport,
+ TrainIcon,
+ },
+ props: {
+ data: {
+ type: Object,
+ required: true,
+ },
+ calendars: {
+ type: Array,
+ required: true,
+ },
+ messageId: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ departureTime() {
+ if (!('departureTime' in this.data.reservationFor)) {
+ return
+ }
+ return moment(this.data.reservationFor.departureTime).format('LT')
+ },
+ departureDate() {
+ if ('departureTime' in this.data.reservationFor) {
+ return moment(this.data.reservationFor.departureTime).format('L')
+ }
+ if ('departureDay' in this.data.reservationFor) {
+ return moment(this.data.reservationFor.departureDay).format('L')
+ }
+ return undefined
+ },
+ arrivalTime() {
+ if (!('arrivalTime' in this.data.reservationFor)) {
+ return
+ }
+ return moment(this.data.reservationFor.arrivalTime).format('LT')
+ },
+ arrivalDate() {
+ if (!('arrivalTime' in this.data.reservationFor)) {
+ return
+ }
+ return moment(this.data.reservationFor.arrivalTime).format('L')
+ },
+ trainNumber() {
+ return this.data.reservationFor.trainNumber
+ },
+ canImport() {
+ return (
+ ('departureTime' in this.data.reservationFor && 'arrivalTime' in this.data.reservationFor) ||
+ 'departureDay' in this.data.reservationFor
+ )
+ },
+ },
+ methods: {
+ handleImport(calendar) {
+ const event = new ical.Component('VEVENT')
+ if ('trainNumber' in this.data.reservationFor) {
+ event.updatePropertyWithValue(
+ 'SUMMARY',
+ t('mail', '{trainNr} from {depStation} to {arrStation}', {
+ trainNr: this.data.reservationFor.trainNumber,
+ depStation: this.data.reservationFor.departureStation.name,
+ arrStation: this.data.reservationFor.arrivalStation.name,
+ })
+ )
+ } else {
+ event.updatePropertyWithValue(
+ 'SUMMARY',
+ t('mail', 'Train from {depStation} to {arrStation}', {
+ depStation: this.data.reservationFor.departureStation.name,
+ arrStation: this.data.reservationFor.arrivalStation.name,
+ })
+ )
+ }
+
+ if ('departureTime' in this.data.reservationFor && 'arrivalTime' in this.data.reservationFor) {
+ const depart = moment(this.data.reservationFor.departureTime).format()
+ event.updatePropertyWithValue('DTSTART', ical.Time.fromDateTimeString(depart))
+ const arrive = moment(this.data.reservationFor.arrivalTime).format()
+ event.updatePropertyWithValue('DTEND', ical.Time.fromDateTimeString(arrive))
+ } else if ('departureDay' in this.data.reservationFor) {
+ const date = moment(this.data.reservationFor.departureDay).format()
+ event.updatePropertyWithValue('DTSTART', ical.Time.fromDateTimeString(date))
+ }
+
+ // TODO: read version from package.json
+ event.updatePropertyWithValue('PRODID', 'Nextcloud Mail')
+
+ // TODO: is this free of collisions? the bug reports will tell us!
+ event.updatePropertyWithValue('UID', md5(this.messageId + this.departureTime))
+
+ const cal = new ical.Component('VCALENDAR')
+ cal.addSubcomponent(event)
+ logger.debug('generated calendar event from train reservation data', {ical: cal.toString()})
+
+ return importCalendarEvent(calendar.url)(cal.toString())
+ .then(() => {
+ logger.debug('event successfully imported')
+ OCP.Toast.success(t('mail', 'Event imported into {calendar}', {calendar: calendar.displayname}))
+ })
+ .catch(error => {
+ logger.error('Could not import event', {error})
+ OCP.Toast.error(t('mail', 'Could not create event'))
+ })
+ },
+ },
+}
+</script>
+
+<style scoped>
+.reservation {
+ display: flex;
+ flex-direction: row;
+ margin: 30px 38px;
+ border: 1px solid var(--color-border);
+ border-radius: var(--border-radius);
+ padding: 20px;
+ align-items: center;
+}
+
+.departure,
+.arrival {
+ display: flex;
+ flex-direction: column;
+ flex-grow: 1;
+}
+
+.departure,
+.arrival,
+.connection {
+ justify-content: center;
+}
+
+.station {
+ font-size: larger;
+ font-weight: bold;
+}
+
+.departure {
+ text-align: right;
+}
+
+.connection {
+ text-align: center;
+ padding: 0 40px;
+}
+</style>
diff --git a/src/service/DAVService.js b/src/service/DAVService.js
index b59d484b7..8682aeae6 100644
--- a/src/service/DAVService.js
+++ b/src/service/DAVService.js
@@ -191,7 +191,7 @@ const splitCalendar = data => {
* @returns {Promise}
*/
export const importCalendarEvent = url => data => {
- Logger.debug('importing event into calendar', {
+ Logger.debug('importing events into calendar', {
url,
data,
})
@@ -202,6 +202,7 @@ export const importCalendarEvent = url => data => {
components.forEach(componentName => {
for (let componentId in file.split[componentName]) {
const component = file.split[componentName][componentId]
+ Logger.info('importing event component', {component})
promises.push(
Promise.resolve(
Axios.put(url + getRandomString() + '.ics', component, {
diff --git a/tests/Integration/KItinerary/ItineraryExtractorTest.php b/tests/Integration/KItinerary/ItineraryExtractorTest.php
new file mode 100644
index 000000000..16aebe5fa
--- /dev/null
+++ b/tests/Integration/KItinerary/ItineraryExtractorTest.php
@@ -0,0 +1,103 @@
+<?php declare(strict_types=1);
+
+/**
+ * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+namespace OCA\Mail\Tests\Unit\KItinerary;
+
+use ChristophWurst\KItinerary\Bin\BinaryAdapter;
+use ChristophWurst\KItinerary\Flatpak\FlatpakAdapter;
+use ChristophWurst\Nextcloud\Testing\TestCase;
+use OCA\Mail\Integration\KItinerary\ItineraryExtractor;
+use OCP\ILogger;
+use PHPUnit\Framework\MockObject\MockObject;
+
+class ItineraryExtractorTest extends TestCase {
+
+ /** @var BinaryAdapter|MockObject */
+ private $binaryAdapter;
+
+ /** @var FlatpakAdapter|MockObject */
+ private $flatpakAdapter;
+
+ /** @var ILogger|MockObject */
+ private $logger;
+
+ /** @var ItineraryExtractor */
+ private $extractor;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->binaryAdapter = $this->createMock(BinaryAdapter::class);
+ $this->flatpakAdapter = $this->createMock(FlatpakAdapter::class);
+ $this->logger = $this->createMock(ILogger::class);
+
+ $this->extractor = new ItineraryExtractor(
+ $this->binaryAdapter,
+ $this->flatpakAdapter,
+ $this->logger
+ );
+ }
+
+ public function testNoAdapterAvailable() {
+ $this->binaryAdapter->expects($this->never())
+ ->method('extractFromString');
+ $this->flatpakAdapter->expects($this->never())
+ ->method('extractFromString');
+
+ $itinerary = $this->extractor->extract('');
+
+ $this->assertEquals([], $itinerary->jsonSerialize());
+ }
+
+ public function testBinAvailable() {
+ $this->binaryAdapter->expects($this->once())
+ ->method('isAvailable')
+ ->willReturn(true);
+ $this->binaryAdapter->expects($this->once())
+ ->method('extractFromString')
+ ->with('data');
+ $this->flatpakAdapter->expects($this->never())
+ ->method('isAvailable');
+ $this->flatpakAdapter->expects($this->never())
+ ->method('extractFromString');
+
+ $itinerary = $this->extractor->extract('data');
+
+ $this->assertEquals([], $itinerary->jsonSerialize());
+ }
+
+ public function testFlatpakAvailable() {
+ $this->binaryAdapter->expects($this->never())
+ ->method('extractFromString');
+ $this->flatpakAdapter->expects($this->once())
+ ->method('isAvailable')
+ ->willReturn(true);
+ $this->flatpakAdapter->expects($this->once())
+ ->method('extractFromString');
+
+ $itinerary = $this->extractor->extract('data');
+
+ $this->assertEquals([], $itinerary->jsonSerialize());
+ }
+
+}
diff --git a/tests/Service/ItineraryServiceTest.php b/tests/Service/ItineraryServiceTest.php
new file mode 100644
index 000000000..00041c46b
--- /dev/null
+++ b/tests/Service/ItineraryServiceTest.php
@@ -0,0 +1,138 @@
+<?php declare(strict_types=1);
+
+/**
+ * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+namespace OCA\Mail\Tests\Service;
+
+use ChristophWurst\KItinerary\Itinerary;
+use ChristophWurst\Nextcloud\Testing\TestCase;
+use Horde_Imap_Client_Socket;
+use OCA\Mail\Account;
+use OCA\Mail\IMAP\IMAPClientFactory;
+use OCA\Mail\IMAP\MessageMapper;
+use OCA\Mail\Integration\KItinerary\ItineraryExtractor;
+use OCA\Mail\Service\ItineraryService;
+use OCP\ICacheFactory;
+use OCP\ILogger;
+use PHPUnit\Framework\MockObject\MockObject;
+
+class ItineraryServiceTest extends TestCase {
+
+ /** @var IMAPClientFactory|MockObject */
+ private $imapClientFactory;
+
+ /** @var MessageMapper|MockObject */
+ private $messageMapper;
+
+ /** @var ItineraryExtractor|MockObject */
+ private $itineraryExtractor;
+
+ /** @var ICacheFactory|MockObject */
+ private $cacheFactor;
+
+ /** @var ItineraryService */
+ private $service;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->imapClientFactory = $this->createMock(IMAPClientFactory::class);
+ $this->messageMapper = $this->createMock(MessageMapper::class);
+ $this->itineraryExtractor = $this->createMock(ItineraryExtractor::class);
+ $this->cacheFactor = $this->createMock(ICacheFactory::class);
+
+ $this->service = new ItineraryService(
+ $this->imapClientFactory,
+ $this->messageMapper,
+ $this->itineraryExtractor,
+ $this->cacheFactor,
+ $this->createMock(ILogger::class)
+ );
+ }
+
+ public function testExtractNoBodyNoAttachments() {
+ /** @var Account|MockObject $account */
+ $account = $this->createMock(Account::class);
+ $this->itineraryExtractor->expects($this->once())
+ ->method('extract')
+ ->willReturn(new Itinerary());
+
+ $itinerary = $this->service->extract($account, 'INBOX', 13);
+
+ $this->assertEquals([], $itinerary->jsonSerialize());
+ }
+
+ public function testExtractFromBody() {
+ /** @var Account|MockObject $account */
+ $account = $this->createMock(Account::class);
+ $client = $this->createMock(Horde_Imap_Client_Socket::class);
+ $this->imapClientFactory->expects($this->once())
+ ->method('getClient')
+ ->with($account)
+ ->willReturn($client);
+ $body = '<html><body>hello</body></html>';
+ $this->messageMapper->expects($this->once())
+ ->method('getHtmlBody')
+ ->with($client, 'INBOX', 13)
+ ->willReturn($body);
+ $this->itineraryExtractor->expects($this->at(0))
+ ->method('extract')
+ ->with($body)
+ ->willReturn(new Itinerary(['datafrombody']));
+ $this->itineraryExtractor->expects($this->at(1))
+ ->method('extract')
+ ->with('["datafrombody"]')
+ ->willReturn(new Itinerary(['datafrombody']));
+
+ $itinerary = $this->service->extract($account, 'INBOX', 13);
+
+ $this->assertEquals(['datafrombody'], $itinerary->jsonSerialize());
+ }
+
+ public function testExtractFromAttachments() {
+ /** @var Account|MockObject $account */
+ $account = $this->createMock(Account::class);
+ $client = $this->createMock(Horde_Imap_Client_Socket::class);
+ $this->imapClientFactory->expects($this->once())
+ ->method('getClient')
+ ->with($account)
+ ->willReturn($client);
+ $pdf = '%PDF-1.3.%';
+ $this->messageMapper->expects($this->once())
+ ->method('getRawAttachments')
+ ->with($client, 'INBOX', 13)
+ ->willReturn([$pdf]);
+ $this->itineraryExtractor->expects($this->at(0))
+ ->method('extract')
+ ->with($pdf)
+ ->willReturn(new Itinerary(['datafrompdf']));
+ $this->itineraryExtractor->expects($this->at(1))
+ ->method('extract')
+ ->with('["datafrompdf"]')
+ ->willReturn(new Itinerary(['datafrompdf']));
+
+ $itinerary = $this->service->extract($account, 'INBOX', 13);
+
+ $this->assertEquals(['datafrompdf'], $itinerary->jsonSerialize());
+ }
+
+}
diff --git a/tests/Unit/Controller/MessagesControllerTest.php b/tests/Unit/Controller/MessagesControllerTest.php
index 75c4fc517..fee9d4c36 100644
--- a/tests/Unit/Controller/MessagesControllerTest.php
+++ b/tests/Unit/Controller/MessagesControllerTest.php
@@ -36,6 +36,7 @@ use OCA\Mail\Mailbox;
use OCA\Mail\Model\IMAPMessage;
use OCA\Mail\Model\Message;
use OCA\Mail\Service\AccountService;
+use OCA\Mail\Service\ItineraryService;
use OCA\Mail\Service\MailManager;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Http;
@@ -67,6 +68,9 @@ class MessagesControllerTest extends TestCase {
/** @var MockObject|IMailSearch */
private $mailSearch;
+ /** @var ItineraryService|MockObject */
+ private $itineraryService;
+
/** @var string */
private $userId;
@@ -111,6 +115,7 @@ class MessagesControllerTest extends TestCase {
$this->accountService = $this->createMock(AccountService::class);
$this->mailManager = $this->createMock(IMailManager::class);
$this->mailSearch = $this->createMock(IMailSearch::class);
+ $this->itineraryService = $this->createMock(ItineraryService::class);
$this->userId = 'john';
$this->userFolder = $this->createMock(Folder::class);
$this->request = $this->createMock(Request::class);
@@ -134,6 +139,7 @@ class MessagesControllerTest extends TestCase {
$this->accountService,
$this->mailManager,
$this->mailSearch,
+ $this->itineraryService,
$this->userId,
$this->userFolder,
$this->logger,