diff --git a/Gemfile b/Gemfile index b079a4019..db01175f8 100644 --- a/Gemfile +++ b/Gemfile @@ -38,6 +38,9 @@ gem 'aasm' # core - authorization gem 'pundit' +# core - image processing +gem 'rszr', '0.5.2' + # performance - Memcached gem 'dalli' @@ -130,8 +133,8 @@ gem 'autodiscover', git: 'https://github.com/zammad-deps/autodiscover' gem 'rubyntlm', git: 'https://github.com/wimm/rubyntlm' gem 'viewpoint' -# image processing -gem 'rszr', '0.5.2' +# integrations - S/MIME +gem 'openssl' # Gems used only for develop/test and not required # in production environments by default. diff --git a/Gemfile.lock b/Gemfile.lock index a1c9ebe5e..a603ebf3c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -355,6 +355,7 @@ GEM omniauth-weibo-oauth2 (0.5.2) omniauth (~> 1.5) omniauth-oauth2 (>= 1.4.0) + openssl (2.1.2) parallel (1.19.1) parser (2.7.1.2) ast (~> 2.4.0) @@ -629,6 +630,7 @@ DEPENDENCIES omniauth-saml omniauth-twitter omniauth-weibo-oauth2 + openssl pg (= 0.21.0) pre-commit pry-rails diff --git a/LICENSE-ICONS-3RD-PARTY.json b/LICENSE-ICONS-3RD-PARTY.json index 80b42fd76..48d19536c 100644 --- a/LICENSE-ICONS-3RD-PARTY.json +++ b/LICENSE-ICONS-3RD-PARTY.json @@ -1,170 +1,135 @@ { - "spinner-small.svg": { - "author": "Felix Niklas", - "url": "", - "license": "MIT" - }, - "sms.svg": { - "author": "Felix Niklas", - "url": "", - "license": "MIT" - }, - "unordered-list.svg": { - "author": "Felix Niklas", - "url": "", - "license": "MIT" - }, - "underline.svg": { - "author": "Felix Niklas", - "url": "", - "license": "MIT" - }, - "strikethrough.svg": { - "author": "Felix Niklas", - "url": "", - "license": "MIT" - }, - "split.svg": { - "author": "Felix Niklas", - "url": "", - "license": "MIT" - }, - "reply.svg": { - "author": "Felix Niklas", - "url": "", - "license": "MIT" - }, - "weibo-button.svg": { - "author": "Weibo", + "archived-modifier.svg": { + "author": "", "url": "", "license": "" }, - "telegram.svg": { - "author": "Telegram", - "url": "", - "license": "" - }, - "twitter-button.svg": { - "author": "Twitter", - "url": "twitter.com", - "license": "" - }, - "zoom-out.svg": { + "arrow-down.svg": { "author": "Felix Niklas", "url": "", "license": "MIT" }, - "zoom-in.svg": { + "arrow-left.svg": { "author": "Felix Niklas", "url": "", "license": "MIT" }, - "web.svg": { - "author": "Zammad", - "url": "", - "license": "MIT" - }, - "user.svg": { - "author": "R\u00e9my M\u00e9dard", - "url": "https:\/\/thenounproject.com\/search\/?q=user&i=10314", - "license": "CC 3.0 Attribution" - }, - "unmute.svg": { + "arrow-right.svg": { "author": "Felix Niklas", "url": "", "license": "MIT" }, - "twitter.svg": { - "author": "Twitter", - "url": "twitter.com", - "license": "" - }, - "trash.svg": { - "author": "Filip Malinowski", - "url": "https:\/\/thenounproject.com\/term\/trash\/16505\/", - "license": "CC 3.0 Attribution" - }, - "total-tickets.svg": { - "author": "Zammad", - "url": "", - "license": "MIT" - }, - "tools.svg": { - "author": "Michael Kussmaul", - "url": "https:\/\/thenounproject.com\/term\/tools\/41655\/", - "license": "CC 3.0 Attribution" - }, - "templates.svg": { + "arrow-up.svg": { "author": "Felix Niklas", "url": "", "license": "MIT" }, - "team.svg": { - "author": "R\u00e9my M\u00e9dard", - "url": "https:\/\/thenounproject.com\/catalarem\/uploads\/?i=2554", - "license": "CC 3.0 Attribution" - }, - "task-state.svg": { - "author": "Zammad", - "url": "", - "license": "MIT" - }, - "switchView.svg": { - "author": "Zammad", - "url": "", - "license": "MIT" - }, - "stopwatch.svg": { - "author": "Zammad", - "url": "", - "license": "MIT" - }, - "status.svg": { - "author": "Zammad", - "url": "", - "license": "MIT" - }, - "status-modified-outer-circle.svg": { - "author": "Zammad", - "url": "", - "license": "MIT" - }, - "file-text.svg": { + "bold.svg": { "author": "Felix Niklas", "url": "", "license": "MIT" }, - "radio.svg": { + "chain.svg": { + "author": "Felix Niklas", + "url": "", + "license": "MIT" + }, + "chat.svg": { + "author": "Felix Niklas", + "url": "", + "license": "MIT" + }, + "checkbox-checked.svg": { "author": "Zammad", "url": "", "license": "MIT" }, - "radio-checked.svg": { + "checkbox-indeterminate.svg": { + "author": "Felix Niklas", + "url": "", + "license": "MIT" + }, + "checkbox.svg": { "author": "Zammad", "url": "", "license": "MIT" }, + "checkmark.svg": { + "author": "Zammad", + "url": "", + "license": "MIT" + }, + "clipboard.svg": { + "author": "Felix Niklas", + "url": "", + "license": "MIT" + }, "clock.svg": { "author": "Felix Niklas", "url": "", "license": "MIT" }, - "important.svg": { + "cloud.svg": { + "author": "Kirill Ulitin", + "url": "https:\/\/thenounproject.com\/search\/?q=cloud&i=84976", + "license": "CC 3.0 Attribution" + }, + "cog.svg": { + "author": "Melvin Salas", + "url": "https:\/\/thenounproject.com\/term\/gear\/17369\/", + "license": "CC 3.0 Attribution" + }, + "crown.svg": { "author": "Felix Niklas", "url": "", "license": "MIT" }, + "danger.svg": { + "author": "", + "url": "", + "license": "" + }, + "dashboard.svg": { + "author": "Anton Gajdosik", + "url": "https:\/\/thenounproject.com\/term\/gauge\/186120", + "license": "CC 3.0 Attribution" + }, "diagonal-cross.svg": { "author": "Zammad", "url": "", "license": "MIT" }, - "lock-open.svg": { + "document.svg": { + "author": "Felix Niklas", + "url": "", + "license": "MIT" + }, + "download.svg": { + "author": "Felix Niklas", + "url": "", + "license": "MIT" + }, + "draft-modifier.svg": { + "author": "", + "url": "", + "license": "" + }, + "draggable.svg": { + "author": "Felix Niklas", + "url": "", + "license": "MIT" + }, + "dropdown-list.svg": { "author": "Zammad", "url": "", "license": "MIT" }, - "rearange.svg": { + "email-button.svg": { + "author": "Felix Niklas", + "url": "", + "license": "MIT" + }, + "email.svg": { "author": "Felix Niklas", "url": "", "license": "MIT" @@ -174,57 +139,107 @@ "url": "", "license": "MIT" }, - "mood-sad.svg": { - "author": "Felix Niklas", - "url": "", - "license": "MIT" - }, - "radio.svg": { - "author": "Zammad", - "url": "", - "license": "MIT" - }, - "radio-checked.svg": { - "author": "Zammad", - "url": "", - "license": "MIT" - }, - "knowledge-base.svg": { - "author": "Felix Niklas", - "url": "", - "license": "MIT" - }, "eye.svg": { "author": "Felix Niklas", "url": "", "license": "MIT" }, - "document.svg": { + "eyedropper.svg": { "author": "Felix Niklas", "url": "", "license": "MIT" }, - "low-priority.svg": { + "facebook-button.svg": { + "author": "Facebook", + "url": "", + "license": "" + }, + "facebook.svg": { + "author": "Facebook", + "url": "", + "license": "" + }, + "file-archive.svg": { "author": "Felix Niklas", "url": "", "license": "MIT" }, - "long-arrow-down.svg": { + "file-code.svg": { "author": "Felix Niklas", "url": "", "license": "MIT" }, - "italic.svg": { + "file-email.svg": { "author": "Felix Niklas", "url": "", "license": "MIT" }, - "inactive-user.svg": { + "file-excel.svg": { "author": "Felix Niklas", "url": "", "license": "MIT" }, - "inactive-organization.svg": { + "file-pdf.svg": { + "author": "Adobe", + "url": "", + "license": "" + }, + "file-powerpoint.svg": { + "author": "Felix Niklas", + "url": "", + "license": "MIT" + }, + "file-text.svg": { + "author": "Felix Niklas", + "url": "", + "license": "MIT" + }, + "file-unknown.svg": { + "author": "Felix Niklas", + "url": "", + "license": "MIT" + }, + "file-word.svg": { + "author": "Felix Niklas", + "url": "", + "license": "MIT" + }, + "form.svg": { + "author": "Pickin Studio", + "url": "https:\/\/thenounproject.com\/search\/?q=website&i=16523", + "license": "CC 3.0 Attribution" + }, + "forward.svg": { + "author": "Felix Niklas", + "url": "", + "license": "MIT" + }, + "full-logo.svg": { + "author": "Zammad", + "url": "", + "license": "MIT" + }, + "github-button.svg": { + "author": "Github", + "url": "", + "license": "" + }, + "gitlab-button.svg": { + "author": "Gitlab", + "url": "", + "license": "" + }, + "google-button.svg": { + "author": "Google", + "url": "", + "license": "" + }, + "group.svg": { + "author": "Felix Niklas", + "url": "", + "license": "MIT" + }, + "help.svg": { "author": "Felix Niklas", "url": "", "license": "MIT" @@ -239,302 +254,17 @@ "url": "", "license": "MIT" }, - "reply-all.svg": { - "author": "Felix Niklas", - "url": "", - "license": "MIT" - }, - "paperclip.svg": { - "author": "Cheesefork", - "url": "https:\/\/thenounproject.com\/search\/?q=attachment&i=197956", - "license": "CC 3.0 Attribution" - }, - "overflow-button.svg": { - "author": "Felix Niklas", - "url": "", - "license": "MIT" - }, - "lock.svg": { + "in-process.svg": { "author": "Zammad", "url": "", "license": "MIT" }, - "lock-open.svg": { - "author": "Zammad", - "url": "", - "license": "MIT" - }, - "forward.svg": { + "inactive-organization.svg": { "author": "Felix Niklas", "url": "", "license": "MIT" }, - "file-word.svg": { - "author": "Felix Niklas", - "url": "", - "license": "MIT" - }, - "file-unknown.svg": { - "author": "Felix Niklas", - "url": "", - "license": "MIT" - }, - "file-powerpoint.svg": { - "author": "Felix Niklas", - "url": "", - "license": "MIT" - }, - "file-pdf.svg": { - "author": "Adobe", - "url": "", - "license": "" - }, - "file-excel.svg": { - "author": "Felix Niklas", - "url": "", - "license": "MIT" - }, - "file-email.svg": { - "author": "Felix Niklas", - "url": "", - "license": "MIT" - }, - "file-code.svg": { - "author": "Felix Niklas", - "url": "", - "license": "MIT" - }, - "file-archive.svg": { - "author": "Felix Niklas", - "url": "", - "license": "MIT" - }, - "office365-button.svg": { - "author": "Office 365", - "url": "", - "license": "" - }, - "logo.svg": { - "author": "Zammad", - "url": "", - "license": "MIT" - }, - "printer.svg": { - "author": "Felix Niklas", - "url": "", - "license": "MIT" - }, - "note.svg": { - "author": "Felix Niklas", - "url": "", - "license": "MIT" - }, - "phone.svg": { - "author": "Michael Zenaty", - "url": "https:\/\/thenounproject.com\/search\/?q=phone&i=21797", - "license": "CC 3.0 Attribution" - }, - "email.svg": { - "author": "Felix Niklas", - "url": "", - "license": "MIT" - }, - "oauth2-button.svg": { - "author": "Felix Niklas", - "url": "", - "license": "MIT" - }, - "linkedin-button.svg": { - "author": "Linkedin", - "url": "", - "license": "" - }, - "google-button.svg": { - "author": "Google", - "url": "", - "license": "" - }, - "gitlab-button.svg": { - "author": "Gitlab", - "url": "", - "license": "" - }, - "github-button.svg": { - "author": "Github", - "url": "", - "license": "" - }, - "facebook-button.svg": { - "author": "Facebook", - "url": "", - "license": "" - }, - "email-button.svg": { - "author": "Felix Niklas", - "url": "", - "license": "MIT" - }, - "line-right-arrow.svg": { - "author": "Felix Niklas", - "url": "", - "license": "MIT" - }, - "small-dot.svg": { - "author": "Zammad", - "url": "", - "license": "MIT" - }, - "signout.svg": { - "author": "Felix Niklas", - "url": "", - "license": "MIT" - }, - "searchdetail.svg": { - "author": "Felix Niklas", - "url": "", - "license": "MIT" - }, - "report.svg": { - "author": "Zammad", - "url": "", - "license": "MIT" - }, - "reopening.svg": { - "author": "Zammad", - "url": "", - "license": "MIT" - }, - "reload.svg": { - "author": "Anand A Nair", - "url": "https:\/\/thenounproject.com\/anandgrafiti\/uploads\/?i=2149", - "license": "CC 3.0 Attribution" - }, - "received-calls.svg": { - "author": "Michael Zenaty", - "url": "https:\/\/thenounproject.com\/search\/?q=phone&i=21797", - "license": "CC 3.0 Attribution" - }, - "plus.svg": { - "author": "Zammad", - "url": "", - "license": "MIT" - }, - "plus-small.svg": { - "author": "Zammad", - "url": "", - "license": "MIT" - }, - "person.svg": { - "author": "Felix Niklas", - "url": "", - "license": "MIT" - }, - "pen.svg": { - "author": "Dmitry Baranovskiy", - "url": "https:\/\/thenounproject.com\/search\/?q=edit&i=5039", - "license": "CC 3.0 Attribution" - }, - "package.svg": { - "author": "Michael Wallner", - "url": "https:\/\/thenounproject.com\/search\/?q=package&i=25152", - "license": "CC 3.0 Attribution" - }, - "overviews.svg": { - "author": "Felix Niklas", - "url": "", - "license": "MIT" - }, - "outbound-calls.svg": { - "author": "Michael Zenaty", - "url": "https:\/\/thenounproject.com\/search\/?q=phone&i=21797", - "license": "CC 3.0 Attribution" - }, - "organization.svg": { - "author": "Felix Niklas", - "url": "", - "license": "MIT" - }, - "one-ticket.svg": { - "author": "Zammad", - "url": "", - "license": "MIT" - }, - "mute.svg": { - "author": "Felix Niklas", - "url": "", - "license": "MIT" - }, - "mood-supergood.svg": { - "author": "Zammad", - "url": "", - "license": "MIT" - }, - "mood-superbad.svg": { - "author": "Zammad", - "url": "", - "license": "MIT" - }, - "mood-ok.svg": { - "author": "Zammad", - "url": "", - "license": "MIT" - }, - "mood-good.svg": { - "author": "Zammad", - "url": "", - "license": "MIT" - }, - "mood-bad.svg": { - "author": "Zammad", - "url": "", - "license": "MIT" - }, - "minus.svg": { - "author": "Zammad", - "url": "", - "license": "MIT" - }, - "minus-small.svg": { - "author": "Zammad", - "url": "", - "license": "MIT" - }, - "message.svg": { - "author": "Felix Niklas", - "url": "", - "license": "MIT" - }, - "marker.svg": { - "author": "Felix Niklas", - "url": "", - "license": "MIT" - }, - "magnifier.svg": { - "author": "Zammad", - "url": "", - "license": "MIT" - }, - "long-arrow-right.svg": { - "author": "Zammad", - "url": "", - "license": "MIT" - }, - "logotype.svg": { - "author": "Zammad", - "url": "", - "license": "MIT" - }, - "loading.svg": { - "author": "Zammad", - "url": "", - "license": "MIT" - }, - "list.svg": { - "author": "Zammad", - "url": "", - "license": "MIT" - }, - "line-left-arrow.svg": { + "inactive-user.svg": { "author": "Felix Niklas", "url": "", "license": "MIT" @@ -544,147 +274,417 @@ "url": "https:\/\/thenounproject.com\/search\/?q=info&i=176431", "license": "CC 3.0 Attribution" }, - "in-process.svg": { - "author": "Zammad", - "url": "", - "license": "MIT" - }, - "help.svg": { - "author": "Felix Niklas", - "url": "", - "license": "MIT" - }, - "group.svg": { - "author": "Felix Niklas", - "url": "", - "license": "MIT" - }, - "full-logo.svg": { - "author": "Zammad", - "url": "", - "license": "MIT" - }, - "form.svg": { - "author": "Pickin Studio", - "url": "https:\/\/thenounproject.com\/search\/?q=website&i=16523", - "license": "CC 3.0 Attribution" - }, - "facebook.svg": { - "author": "Facebook", + "internal-modifier.svg": { + "author": "", "url": "", "license": "" }, - "eyedropper.svg": { + "italic.svg": { "author": "Felix Niklas", "url": "", "license": "MIT" }, - "dropdown-list.svg": { + "knowledge-base-answer.svg": { + "author": "", + "url": "", + "license": "" + }, + "knowledge-base.svg": { + "author": "Felix Niklas", + "url": "", + "license": "MIT" + }, + "line-left-arrow.svg": { + "author": "Felix Niklas", + "url": "", + "license": "MIT" + }, + "line-right-arrow.svg": { + "author": "Felix Niklas", + "url": "", + "license": "MIT" + }, + "linkedin-button.svg": { + "author": "Linkedin", + "url": "", + "license": "" + }, + "list.svg": { "author": "Zammad", "url": "", "license": "MIT" }, - "draggable.svg": { + "loading.svg": { + "author": "Zammad", + "url": "", + "license": "MIT" + }, + "lock-open.svg": { + "author": "Zammad", + "url": "", + "license": "MIT" + }, + "lock.svg": { + "author": "Zammad", + "url": "", + "license": "MIT" + }, + "logo.svg": { + "author": "Zammad", + "url": "", + "license": "MIT" + }, + "logotype.svg": { + "author": "Zammad", + "url": "", + "license": "MIT" + }, + "long-arrow-down.svg": { "author": "Felix Niklas", "url": "", "license": "MIT" }, - "download.svg": { + "long-arrow-right.svg": { + "author": "Zammad", + "url": "", + "license": "MIT" + }, + "low-priority.svg": { "author": "Felix Niklas", "url": "", "license": "MIT" }, - "dashboard.svg": { - "author": "Anton Gajdosik", - "url": "https:\/\/thenounproject.com\/term\/gauge\/186120", + "magnifier.svg": { + "author": "Zammad", + "url": "", + "license": "MIT" + }, + "marker.svg": { + "author": "Felix Niklas", + "url": "", + "license": "MIT" + }, + "message.svg": { + "author": "Felix Niklas", + "url": "", + "license": "MIT" + }, + "minus-small.svg": { + "author": "Zammad", + "url": "", + "license": "MIT" + }, + "minus.svg": { + "author": "Zammad", + "url": "", + "license": "MIT" + }, + "mood-bad.svg": { + "author": "Zammad", + "url": "", + "license": "MIT" + }, + "mood-good.svg": { + "author": "Zammad", + "url": "", + "license": "MIT" + }, + "mood-ok.svg": { + "author": "Zammad", + "url": "", + "license": "MIT" + }, + "mood-sad.svg": { + "author": "Felix Niklas", + "url": "", + "license": "MIT" + }, + "mood-superbad.svg": { + "author": "Zammad", + "url": "", + "license": "MIT" + }, + "mood-supergood.svg": { + "author": "Zammad", + "url": "", + "license": "MIT" + }, + "mute.svg": { + "author": "Felix Niklas", + "url": "", + "license": "MIT" + }, + "not-signed.svg": { + "author": "Zammad", + "url": "", + "license": "MIT" + }, + "note.svg": { + "author": "Felix Niklas", + "url": "", + "license": "MIT" + }, + "oauth2-button.svg": { + "author": "Felix Niklas", + "url": "", + "license": "MIT" + }, + "office365-button.svg": { + "author": "Office 365", + "url": "", + "license": "" + }, + "one-ticket.svg": { + "author": "Zammad", + "url": "", + "license": "MIT" + }, + "organization.svg": { + "author": "Felix Niklas", + "url": "", + "license": "MIT" + }, + "outbound-calls.svg": { + "author": "Michael Zenaty", + "url": "https:\/\/thenounproject.com\/search\/?q=phone&i=21797", "license": "CC 3.0 Attribution" }, - "crown.svg": { + "overflow-button.svg": { "author": "Felix Niklas", "url": "", "license": "MIT" }, - "cog.svg": { - "author": "Melvin Salas", - "url": "https:\/\/thenounproject.com\/term\/gear\/17369\/", + "overviews.svg": { + "author": "Felix Niklas", + "url": "", + "license": "MIT" + }, + "package.svg": { + "author": "Michael Wallner", + "url": "https:\/\/thenounproject.com\/search\/?q=package&i=25152", "license": "CC 3.0 Attribution" }, - "cloud.svg": { - "author": "Kirill Ulitin", - "url": "https:\/\/thenounproject.com\/search\/?q=cloud&i=84976", + "paperclip.svg": { + "author": "Cheesefork", + "url": "https:\/\/thenounproject.com\/search\/?q=attachment&i=197956", "license": "CC 3.0 Attribution" }, - "clipboard.svg": { + "pen.svg": { + "author": "Dmitry Baranovskiy", + "url": "https:\/\/thenounproject.com\/search\/?q=edit&i=5039", + "license": "CC 3.0 Attribution" + }, + "person.svg": { "author": "Felix Niklas", "url": "", "license": "MIT" }, - "bold.svg": { - "author": "Felix Niklas", - "url": "", - "license": "MIT" + "phone.svg": { + "author": "Michael Zenaty", + "url": "https:\/\/thenounproject.com\/search\/?q=phone&i=21797", + "license": "CC 3.0 Attribution" }, - "checkbox.svg": { + "plus-small.svg": { "author": "Zammad", "url": "", "license": "MIT" }, - "checkbox-indeterminate.svg": { - "author": "Felix Niklas", - "url": "", - "license": "MIT" - }, - "checkmark.svg": { + "plus.svg": { "author": "Zammad", "url": "", "license": "MIT" }, - "chain.svg": { + "printer.svg": { "author": "Felix Niklas", "url": "", "license": "MIT" }, - "bold.svg": { - "author": "Felix Niklas", - "url": "", - "license": "MIT" - }, - "checkbox.svg": { + "radio-checked.svg": { "author": "Zammad", "url": "", "license": "MIT" }, - "checkbox-indeterminate.svg": { - "author": "Felix Niklas", - "url": "", - "license": "MIT" - }, - "checkbox-checked.svg": { + "radio.svg": { "author": "Zammad", "url": "", "license": "MIT" }, - "arrow-down.svg": { + "rearange.svg": { "author": "Felix Niklas", "url": "", "license": "MIT" }, - "arrow-up.svg": { + "received-calls.svg": { + "author": "Michael Zenaty", + "url": "https:\/\/thenounproject.com\/search\/?q=phone&i=21797", + "license": "CC 3.0 Attribution" + }, + "reload.svg": { + "author": "Anand A Nair", + "url": "https:\/\/thenounproject.com\/anandgrafiti\/uploads\/?i=2149", + "license": "CC 3.0 Attribution" + }, + "reopening.svg": { + "author": "Zammad", + "url": "", + "license": "MIT" + }, + "reply-all.svg": { "author": "Felix Niklas", "url": "", "license": "MIT" }, - "arrow-right.svg": { + "reply.svg": { "author": "Felix Niklas", "url": "", "license": "MIT" }, - "arrow-left.svg": { + "report.svg": { + "author": "Zammad", + "url": "", + "license": "MIT" + }, + "searchdetail.svg": { "author": "Felix Niklas", "url": "", "license": "MIT" }, - "chat.svg": { + "signed.svg": { + "author": "Zammad", + "url": "", + "license": "MIT" + }, + "signout.svg": { + "author": "Felix Niklas", + "url": "", + "license": "MIT" + }, + "small-dot.svg": { + "author": "Zammad", + "url": "", + "license": "MIT" + }, + "sms.svg": { + "author": "Felix Niklas", + "url": "", + "license": "MIT" + }, + "spinner-small.svg": { + "author": "Felix Niklas", + "url": "", + "license": "MIT" + }, + "split.svg": { + "author": "Felix Niklas", + "url": "", + "license": "MIT" + }, + "status-modified-outer-circle.svg": { + "author": "Zammad", + "url": "", + "license": "MIT" + }, + "status.svg": { + "author": "Zammad", + "url": "", + "license": "MIT" + }, + "stopwatch.svg": { + "author": "Zammad", + "url": "", + "license": "MIT" + }, + "strikethrough.svg": { + "author": "Felix Niklas", + "url": "", + "license": "MIT" + }, + "switchView.svg": { + "author": "Zammad", + "url": "", + "license": "MIT" + }, + "task-state.svg": { + "author": "Zammad", + "url": "", + "license": "MIT" + }, + "team.svg": { + "author": "R\u00e9my M\u00e9dard", + "url": "https:\/\/thenounproject.com\/catalarem\/uploads\/?i=2554", + "license": "CC 3.0 Attribution" + }, + "telegram.svg": { + "author": "Telegram", + "url": "", + "license": "" + }, + "templates.svg": { + "author": "Felix Niklas", + "url": "", + "license": "MIT" + }, + "tools.svg": { + "author": "Michael Kussmaul", + "url": "https:\/\/thenounproject.com\/term\/tools\/41655\/", + "license": "CC 3.0 Attribution" + }, + "total-tickets.svg": { + "author": "Zammad", + "url": "", + "license": "MIT" + }, + "trash.svg": { + "author": "Filip Malinowski", + "url": "https:\/\/thenounproject.com\/term\/trash\/16505\/", + "license": "CC 3.0 Attribution" + }, + "twitter-button.svg": { + "author": "Twitter", + "url": "twitter.com", + "license": "" + }, + "twitter.svg": { + "author": "Twitter", + "url": "twitter.com", + "license": "" + }, + "underline.svg": { + "author": "Felix Niklas", + "url": "", + "license": "MIT" + }, + "unmute.svg": { + "author": "Felix Niklas", + "url": "", + "license": "MIT" + }, + "unordered-list.svg": { + "author": "Felix Niklas", + "url": "", + "license": "MIT" + }, + "user.svg": { + "author": "R\u00e9my M\u00e9dard", + "url": "https:\/\/thenounproject.com\/search\/?q=user&i=10314", + "license": "CC 3.0 Attribution" + }, + "web.svg": { + "author": "Zammad", + "url": "", + "license": "MIT" + }, + "weibo-button.svg": { + "author": "Weibo", + "url": "", + "license": "" + }, + "zoom-in.svg": { + "author": "Felix Niklas", + "url": "", + "license": "MIT" + }, + "zoom-out.svg": { "author": "Felix Niklas", "url": "", "license": "MIT" diff --git a/app/assets/javascripts/app/controllers/_integration/smime.coffee b/app/assets/javascripts/app/controllers/_integration/smime.coffee new file mode 100644 index 000000000..542f8bbc1 --- /dev/null +++ b/app/assets/javascripts/app/controllers/_integration/smime.coffee @@ -0,0 +1,258 @@ +class Index extends App.ControllerIntegrationBase + featureIntegration: 'smime_integration' + featureName: 'S/MIME' + featureConfig: 'smime_config' + description: [ + ['S/MIME (Secure/Multipurpose Internet Mail Extensions) is a widely accepted method (or more precisely, a protocol) for sending digitally signed and encrypted messages.'] + ] + events: + 'change .js-switch input': 'switch' + + render: => + super + new Form( + el: @$('.js-form') + ) + + new App.HttpLog( + el: @$('.js-log') + facility: 'S/MIME' + ) + +class Form extends App.Controller + events: + 'click .js-addCertificate': 'addCertificate' + 'click .js-addPrivateKey': 'addPrivateKey' + 'click .js-updateGroup': 'updateGroup' + + constructor: -> + super + @render() + + currentConfig: -> + App.Setting.get('smime_config') + + setConfig: (value) -> + App.Setting.set('smime_config', value, {notify: true}) + + render: => + @config = @currentConfig() + + @html App.view('integration/smime')( + config: @config + ) + @certList() + @groupList() + + certList: => + new List(el: @$('.js-certList')) + + groupList: => + new Group( + el: @$('.js-groupList') + config: @config + ) + + addCertificate: => + new Certificate( + callback: @list + ) + + addPrivateKey: => + new PrivateKey( + callback: @list + ) + + updateGroup: (e) => + params = App.ControllerForm.params(e) + @setConfig(params) + +class Certificate extends App.ControllerModal + buttonClose: true + buttonCancel: true + buttonSubmit: 'Add' + autoFocusOnFirstInput: false + head: 'Add Certificate' + large: true + + content: -> + + # show start dialog + content = $(App.view('integration/smime_certificate_add')( + head: 'Add Certificate' + )) + content + + onSubmit: (e) => + params = new FormData($(e.currentTarget).closest('form').get(0)) + params.set('try', true) + if _.isEmpty(params.get('data')) + params.delete('data') + @formDisable(e) + + @ajax( + id: 'smime-certificate-add' + type: 'POST' + url: "#{@apiPath}/integration/smime/certificate" + processData: false + contentType: false + cache: false + data: params + success: (data, status, xhr) => + console.log('success') + @close() + @callback() + error: (data) => + console.log('error') + @close() + details = data.responseJSON || {} + @notify + type: 'error' + msg: App.i18n.translateContent(details.error_human || details.error || 'Unable to import!') + timeout: 6000 + ) + +class PrivateKey extends App.ControllerModal + buttonClose: true + buttonCancel: true + buttonSubmit: 'Add' + autoFocusOnFirstInput: false + head: 'Add Private Key' + large: true + + content: -> + + # show start dialog + content = $(App.view('integration/smime_private_key_add')( + head: 'Add Private Key' + )) + content + + onSubmit: (e) => + params = new FormData($(e.currentTarget).closest('form').get(0)) + params.set('try', true) + if _.isEmpty(params.get('data')) + params.delete('data') + @formDisable(e) + + @ajax( + id: 'smime-private_key-add' + type: 'POST' + url: "#{@apiPath}/integration/smime/private_key" + processData: false + contentType: false + cache: false + data: params + success: (data, status, xhr) => + @close() + @callback() + error: (data) => + @close() + details = data.responseJSON || {} + @notify + type: 'error' + msg: App.i18n.translateContent(details.error_human || details.error || 'Unable to import!') + timeout: 6000 + ) + + +class List extends App.Controller + events: + 'click .js-remove': 'remove' + + constructor: -> + super + @load() + + load: => + @ajax( + id: 'smime-list' + type: 'GET' + url: "#{@apiPath}/integration/smime/certificate" + success: (data, status, xhr) => + @render(data) + + error: (data, status) => + + # do not close window if request is aborted + return if status is 'abort' + + details = data.responseJSON || {} + @notify( + type: 'error' + msg: App.i18n.translateContent(details.error_human || details.error || 'Unable to load list of certificates!') + ) + + # do something + ) + + render: (data) => + @html App.view('integration/smime_list')( + certs: data + ) + + remove: (e) => + e.preventDefault() + id = $(e.currentTarget).parents('tr').data('id') + return if !id + + @ajax( + id: 'smime-list' + type: 'DELETE' + url: "#{@apiPath}/integration/smime/certificate" + data: JSON.stringify(id: id) + success: (data, status, xhr) => + @load() + + error: (data, status) => + + # do not close window if request is aborted + return if status is 'abort' + + details = data.responseJSON || {} + @notify( + type: 'error' + msg: App.i18n.translateContent(details.error_human || details.error || 'Unable to save!') + ) + ) + +class Group extends App.Controller + constructor: -> + super + @render() + + render: (data) => + groups = App.Group.search(sortBy: 'name', filter: active: true) + @html App.view('integration/smime_group')( + groups: groups + ) + for group in groups + for type, selector of { default_sign: 'js-signDefault', default_encryption: 'js-encryptionDefault' } + selected = true + if @config?.group_id && @config.group_id[type] + selected = @config.group_id[type][group.id.toString()] + selection = App.UiElement.boolean.render( + name: "group_id::#{type}::#{group.id}" + multiple: false + null: false + nulloption: false + value: selected + class: 'form-control--small' + ) + @$("[data-id=#{group.id}] .#{selector}").html(selection) + +class State + @current: -> + App.Setting.get('smime_integration') + +App.Config.set( + 'Integrationsmime' + { + name: 'S/MIME' + target: '#system/integration/smime' + description: 'S/MIME enables you to send digitally signed and encrypted messages.' + controller: Index + state: State + } + 'NavBarIntegrations' +) diff --git a/app/assets/javascripts/app/controllers/_ui_element/ticket_perform_action.coffee b/app/assets/javascripts/app/controllers/_ui_element/ticket_perform_action.coffee index c7d2002e6..b1ee3689c 100644 --- a/app/assets/javascripts/app/controllers/_ui_element/ticket_perform_action.coffee +++ b/app/assets/javascripts/app/controllers/_ui_element/ticket_perform_action.coffee @@ -428,6 +428,35 @@ class App.UiElement.ticket_perform_action elementRow.find('.js-setNotification').html(notificationElement).removeClass('hide') + if App.Config.get('smime_integration') == true + selection = App.UiElement.select.render( + name: "#{name}::sign" + multiple: false + options: { + 'no': 'Do not sign email' + 'discard': 'Sign email (if not possible, discard notification)' + 'always': 'Sign email (if not possible, send notification anyway)' + } + value: meta.sign + translate: true + ) + + elementRow.find('.js-sign').html(selection) + + selection = App.UiElement.select.render( + name: "#{name}::encryption" + multiple: false + options: { + 'no': 'Do not encrypt email' + 'discard': 'Encrypt email (if not possible, discard notification)' + 'always': 'Encrypt email (if not possible, send notification anyway)' + } + value: meta.encryption + translate: true + ) + + elementRow.find('.js-encryption').html(selection) + @buildArticleArea: (articleType, elementFull, elementRow, groupAndAttribute, elements, meta, attribute) -> return if elementRow.find(".js-setArticle .js-body-#{articleType}").get(0) diff --git a/app/assets/javascripts/app/controllers/agent_ticket_create.coffee b/app/assets/javascripts/app/controllers/agent_ticket_create.coffee index cd828e6c3..44774e636 100644 --- a/app/assets/javascripts/app/controllers/agent_ticket_create.coffee +++ b/app/assets/javascripts/app/controllers/agent_ticket_create.coffee @@ -1,11 +1,14 @@ class App.TicketCreate extends App.Controller + @include App.SecurityOptions + elements: '.tabsSidebar': 'sidebar' events: - 'click .type-tabs .tab': 'changeFormType' - 'submit form': 'submit' - 'click .js-cancel': 'cancel' + 'click .type-tabs .tab': 'changeFormType' + 'submit form': 'submit' + 'click .js-cancel': 'cancel' + 'click .js-active-toggle': 'toggleButton' types: { 'phone-in': { @@ -121,12 +124,25 @@ class App.TicketCreate extends App.Controller # force changing signature @$('[name="group_id"]').trigger('change') + # add observer to change options + @$('[name="cc"], [name="group_id"], [name="customer_id"]').bind('change', => + @updateSecurityOptions() + ) + @updateSecurityOptions() + # show cc if type is 'email-out' @$('[name="cc"]').closest('.form-group').removeClass('hide') + + if @securityEnabled() + @securityOptionsShow() + else @$('[name="cc"]').closest('.form-group').addClass('hide') + if @securityEnabled() + @securityOptionsHide() + # show notice @$('.js-note').addClass('hide') @$(".js-note[data-type='#{type}']").removeClass('hide') @@ -165,6 +181,13 @@ class App.TicketCreate extends App.Controller return false if !diff || _.isEmpty(diff) return true + updateSecurityOptions: => + params = @params() + if params.customer_id_completion + params.to = params.customer_id_completion + + @updateSecurityOptionsRemote(@taskKey, params, params, @paramsSecurity()) + autosaveStop: => @clearDelay('ticket-create-form-update') @el.off('change.local blur.local keyup.local paste.local input.local') @@ -385,6 +408,9 @@ class App.TicketCreate extends App.Controller @tokanice() + toggleButton: (event) -> + @$(event.currentTarget).toggleClass('btn--active') + tokanice: -> App.Utils.tokanice('.content.active input[name=cc]', 'email') @@ -447,7 +473,7 @@ class App.TicketCreate extends App.Controller # create article if sender.name is 'Customer' - params['article'] = { + params.article = { to: (group && group.name) || '' from: params.customer_id_completion cc: params.cc @@ -459,7 +485,7 @@ class App.TicketCreate extends App.Controller content_type: 'text/html' } else - params['article'] = { + params.article = { from: (group && group.name) || '' to: params.customer_id_completion cc: params.cc @@ -471,6 +497,11 @@ class App.TicketCreate extends App.Controller content_type: 'text/html' } + # add security params + if @securityOptionsShown() + params.article.preferences ||= {} + params.article.preferences.security = @paramsSecurity() + ticket.load(params) ticketErrorsTop = ticket.validate( diff --git a/app/assets/javascripts/app/controllers/ticket_zoom.coffee b/app/assets/javascripts/app/controllers/ticket_zoom.coffee index cb147cc45..a6e685d1d 100644 --- a/app/assets/javascripts/app/controllers/ticket_zoom.coffee +++ b/app/assets/javascripts/app/controllers/ticket_zoom.coffee @@ -709,12 +709,11 @@ class App.TicketZoom extends App.Controller markFormDiff: (diff = {}) => ticketForm = @$('.edit') ticketSidebar = @$('.tabsSidebar-tab[data-tab="ticket"]') - articleForm = @$('.article-add') resetButton = @$('.js-reset') params = {} - params.ticket = @forRemoveMeta(@formParam(ticketForm)) - params.article = @forRemoveMeta(@formParam(articleForm)) + params.ticket = @forRemoveMeta(@ticketParams()) + params.article = @forRemoveMeta(@articleNew.params()) # clear all changes if _.isEmpty(diff.ticket) && _.isEmpty(diff.article) @@ -743,6 +742,9 @@ class App.TicketZoom extends App.Controller resetButton.removeClass('hide') + ticketParams: => + @formParam(@$('.edit')) + submitDisable: (e) => if e @formDisable(e) @@ -767,7 +769,7 @@ class App.TicketZoom extends App.Controller @submitEnable(e) return - ticketParams = @formParam(@$('.edit')) + ticketParams = @ticketParams() articleParams = @articleNew.params() # validate ticket diff --git a/app/assets/javascripts/app/controllers/ticket_zoom/article_action/email_reply.coffee b/app/assets/javascripts/app/controllers/ticket_zoom/article_action/email_reply.coffee index 6144e0254..e9b19457d 100644 --- a/app/assets/javascripts/app/controllers/ticket_zoom/article_action/email_reply.coffee +++ b/app/assets/javascripts/app/controllers/ticket_zoom/article_action/email_reply.coffee @@ -253,7 +253,7 @@ class EmailReply extends App.Controller icon: 'email' attributes: attributes internal: false, - features: ['attachment'] + features: ['attachment', 'security'] } articleTypes diff --git a/app/assets/javascripts/app/controllers/ticket_zoom/article_new.coffee b/app/assets/javascripts/app/controllers/ticket_zoom/article_new.coffee index c63f65c72..5c07d6f52 100644 --- a/app/assets/javascripts/app/controllers/ticket_zoom/article_new.coffee +++ b/app/assets/javascripts/app/controllers/ticket_zoom/article_new.coffee @@ -1,4 +1,6 @@ class App.TicketZoomArticleNew extends App.Controller + @include App.SecurityOptions + elements: '.js-textarea': 'textarea' '.attachmentPlaceholder': 'attachmentPlaceholder' @@ -24,6 +26,7 @@ class App.TicketZoomArticleNew extends App.Controller 'click .list-entry-type div': 'changeType' 'focus .js-textarea': 'openTextarea' 'input .js-textarea': 'updateLetterCount' + 'click .js-active-toggle': 'toggleButton' constructor: -> super @@ -106,6 +109,12 @@ class App.TicketZoomArticleNew extends App.Controller @render() ) + # update security options + @bind('ui::ticket::updateSecurityOptions', (data) => + return if data.taskKey isnt @taskKey + @updateSecurityOptions() + ) + tokanice: (type = 'email') -> App.Utils.tokanice('.content.active .js-to, .js-cc, js-bcc', type) @@ -294,6 +303,11 @@ class App.TicketZoomArticleNew extends App.Controller params.body = "#{params.body}\n#{@signature.text()}" break + # add security params + if @securityOptionsShown() + params.preferences ||= {} + params.preferences.security = @paramsSecurity() + params validate: => @@ -417,6 +431,15 @@ class App.TicketZoomArticleNew extends App.Controller @warningTextLength = articleType.warningTextLength @delay(@updateLetterCount, 600) @$('.js-textSizeLimit').removeClass('hide') + if name is 'security' + if @securityEnabled() + @securityOptionsShow() + + # add observer to change options + @$('.js-to, .js-cc').bind('change', => + @updateSecurityOptions() + ) + @updateSecurityOptions() # convert remote src images to data uri @$('[data-name=body] img').each( (i,image) -> @@ -434,6 +457,9 @@ class App.TicketZoomArticleNew extends App.Controller @scrollToBottom() if wasScrolledToBottom + updateSecurityOptions: => + @updateSecurityOptionsRemote(@ticket.id, @ui.ticketParams(), @params(), @paramsSecurity()) + setArticleTypePost: (type, signaturePosition = 'bottom') => for localConfig in @actions() if localConfig && localConfig.setArticleTypePost @@ -625,3 +651,6 @@ class App.TicketZoomArticleNew extends App.Controller if localConfig actions.push localConfig actions + + toggleButton: (event) -> + @$(event.currentTarget).toggleClass('btn--active') diff --git a/app/assets/javascripts/app/controllers/ticket_zoom/article_view.coffee b/app/assets/javascripts/app/controllers/ticket_zoom/article_view.coffee index d83e03dff..09befde85 100644 --- a/app/assets/javascripts/app/controllers/ticket_zoom/article_view.coffee +++ b/app/assets/javascripts/app/controllers/ticket_zoom/article_view.coffee @@ -81,11 +81,13 @@ class ArticleViewItem extends App.ObserverController '.textBubble-overflowContainer': 'textBubbleOverflowContainer' events: - 'click .textBubble': 'toggleMetaWithDelay' - 'click .textBubble a': 'stopPropagation' - 'click .js-toggleFold': 'toggleFold' - 'click .richtext-content img': 'imageView' - 'click .attachments img': 'imageView' + 'click .article-meta-permanent': 'toggleMetaWithDelay' + 'click .textBubble': 'toggleMetaWithDelay' + 'click .textBubble a': 'stopPropagation' + 'click .js-toggleFold': 'toggleFold' + 'click .richtext-content img': 'imageView' + 'click .attachments img': 'imageView' + 'click .js-securityRetryProcess': 'retrySecurityProcess' constructor: -> super @@ -296,6 +298,46 @@ class ArticleViewItem extends App.ObserverController else bubbleOverflowContainer.addClass('hide') + retrySecurityProcess: (e) -> + e.preventDefault() + e.stopPropagation() + + article_id = $(e.target).closest('.ticket-article-item').data('id') + + @ajax( + id: 'retrySecurityProcess' + type: 'POST' + url: "#{@apiPath}/ticket_articles/#{article_id}/retry_security_process" + processData: true + success: (data, status, xhr) => + if data.sign.success + @notify + type: 'success' + msg: App.i18n.translateContent('Verify sign success!') + else if data.sign.comment + comment = App.i18n.translateContent('Verify sign failed!') + ' ' + App.i18n.translateContent(data.sign.comment || '') + @notify + type: 'error' + msg: comment + timeout: 2000 + + if data.encryption.success + @notify + type: 'success' + msg: App.i18n.translateContent('Decryption success!') + else if data.encryption.comment + comment = App.i18n.translateContent('Decryption failed!') + ' ' + App.i18n.translateContent(data.encryption.comment || '') + @notify + type: 'error' + msg: comment + timeout: 2000 + + error: (xhr) => + @notify + type: 'error' + msg: App.i18n.translateContent('Retry security process failed!') + ) + stopPropagation: (e) -> e.stopPropagation() diff --git a/app/assets/javascripts/app/controllers/ticket_zoom/form_handler_security_options.coffee b/app/assets/javascripts/app/controllers/ticket_zoom/form_handler_security_options.coffee new file mode 100644 index 000000000..2ff5ca738 --- /dev/null +++ b/app/assets/javascripts/app/controllers/ticket_zoom/form_handler_security_options.coffee @@ -0,0 +1,10 @@ +class TicketZoomFormHandleSecurityOptions + + # central method, is getting called on every ticket form change + # but only trigger event for group_id changes + @run: (params, attribute, attributes, classname, form, ui) -> + + return if attribute.name isnt 'group_id' + App.Event.trigger('ui::ticket::updateSecurityOptions', { taskKey: ui.taskKey }) + +App.Config.set('140-ticketFormSecurityOptions', TicketZoomFormHandleSecurityOptions, 'TicketZoomFormHandler') diff --git a/app/assets/javascripts/app/lib/mixins/security_options.coffee b/app/assets/javascripts/app/lib/mixins/security_options.coffee new file mode 100644 index 000000000..47348cd9a --- /dev/null +++ b/app/assets/javascripts/app/lib/mixins/security_options.coffee @@ -0,0 +1,89 @@ +# Methods for displaying security ui elements and to get security params + +App.SecurityOptions = + + securityOptionsShow: -> + @$('.js-securityOptions').removeClass('hide') + + securityOptionsHide: -> + @$('.js-securityOptions').addClass('hide') + + securityOptionsShown: -> + !@$('.js-securityOptions').hasClass('hide') + + securityEnabled: -> + App.Config.get('smime_integration') + + paramsSecurity: => + if @$('.js-securityOptions').hasClass('hide') + return {} + + security = {} + security.encryption ||= {} + security.sign ||= {} + security.type = 'S/MIME' + if @$('.js-securityEncrypt').hasClass('btn--active') + security.encryption.success = true + if @$('.js-securitySign').hasClass('btn--active') + security.sign.success = true + security + + updateSecurityOptionsRemote: (key, ticket, article, securityOptions) -> + callback = => + @ajax( + id: "smime-check-#{key}" + type: 'POST' + url: "#{@apiPath}/integration/smime" + data: JSON.stringify(ticket: ticket, article: article) + processData: true + success: (data, status, xhr) => + + # get default selected security options + selected = + encryption: true + sign: true + smimeConfig = App.Config.get('smime_config') + for type, selector of { default_sign: 'sign', default_encryption: 'encryption' } + if smimeConfig?.group_id?[type] && ticket.group_id + if smimeConfig.group_id[type][ticket.group_id.toString()] == false + selected[selector] = false + + @$('.js-securityEncrypt').attr('title', data.encryption.comment) + + # if encryption is possible + if data.encryption.success is true + @$('.js-securityEncrypt').attr('disabled', false) + + # overrule current selection with Group configuration + if selected.encryption + @$('.js-securityEncrypt').addClass('btn--active') + else + @$('.js-securityEncrypt').removeClass('btn--active') + + # if encryption is not possible + else + @$('.js-securityEncrypt').attr('disabled', true) + @$('.js-securityEncrypt').removeClass('btn--active') + + @$('.js-securitySign').attr('title', data.sign.comment) + + # if sign is possible + if data.sign.success is true + @$('.js-securitySign').attr('disabled', false) + + # overrule current selection with Group configuration + if selected.sign + @$('.js-securitySign').addClass('btn--active') + else + @$('.js-securitySign').removeClass('btn--active') + + # if sign is possible + else + @$('.js-securitySign').attr('disabled', true) + @$('.js-securitySign').removeClass('btn--active') + + error: (data) -> + details = data.responseJSON || {} + console.log(details) + ) + @delay(callback, 200, 'security-check') diff --git a/app/assets/javascripts/app/views/agent_ticket_create.jst.eco b/app/assets/javascripts/app/views/agent_ticket_create.jst.eco index f37872bcc..fef02088e 100644 --- a/app/assets/javascripts/app/views/agent_ticket_create.jst.eco +++ b/app/assets/javascripts/app/views/agent_ticket_create.jst.eco @@ -27,10 +27,17 @@