Compare commits

...

1883 Commits
1.8.79 ... dist

Author SHA1 Message Date
FreeScout
f92cb048fe Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2026-10-03 03:20:09 -07:00
FreeScout
917b4e0ce1 v1.8.245 2026-10-03 03:19:50 -07:00
FreeScout
68fe730581 Merge pull request #5696 from il2020/master
Fix "Class Symfony\Component\Filesystem\Filesystem not found" in Helper::isAbasolutePath()
2026-10-03 13:08:39 +03:00
il2020
c2c2de734e Update Helper.php 2026-10-03 11:20:46 +04:00
FreeScout
628e3ae5b5 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2026-10-02 21:29:55 -07:00
FreeScout
7b5ab22381 v1.8.244 2026-10-02 21:29:43 -07:00
FreeScout
7f0e612a15 Add Customer Visibility option to Settings 2026-10-02 06:35:24 -07:00
FreeScout
65e963e163 Customers table view mode 2026-10-02 04:22:53 -07:00
FreeScout
ae038154db Style adjustments 2026-10-02 03:22:52 -07:00
FreeScout
dcd178b3d5 Fit wide tables in thread content #5694 from il2020/master
Fit wide tables in thread content
2026-10-02 13:13:33 +03:00
FreeScout
f594564630 Open the next active conversation after deleting, per the after-send setting #5693 from JayOfTheKeyboard/feature/delete-open-next
Open the next active conversation after deleting, per the after-send setting
2026-10-02 12:42:33 +03:00
il2020
73e2d260a7 Fit wide tables in thread content 2026-10-02 13:38:19 +04:00
FreeScout
2eb76242a6 Focus the primary button in confirmation dialogs so Enter confirms #5692 from JayOfTheKeyboard/fix/confirm-dialog-enter
Focus the primary button in confirmation dialogs so Enter confirms
2026-10-02 12:34:31 +03:00
FreeScout
405c9de596 Update pt-PT translation 2026-10-01 04:16:39 -07:00
FreeScout
9921987a88 Remove target=_blank from non-external links - closes #5086 2026-10-01 04:01:56 -07:00
FreeScout
24f35e170e Change customer when received a reply to a conversation from completely new email address - closes #5690 2026-09-30 11:41:17 -07:00
FreeScout
659e6e8813 Fix creating lists in editor - closes #5681 2026-09-30 07:39:35 -07:00
JayOfTheKeyboard
c299a6e4b0 Open the next active conversation after deleting, per the after-send setting
After deleting a conversation FreeScout always returns to the folder. When the user's after-send setting is "Next active conversation", open the next active conversation instead, as changing the status already does. Other settings keep returning to the folder. The next conversation is found before the delete, while the conversation is still in its folder.
2026-09-30 20:43:50 +10:00
JayOfTheKeyboard
544829d7ea Focus the primary button in confirmation dialogs so Enter confirms
Confirmation dialogs opened with showModalDialog() leave focus on the modal container, so Enter does nothing and Tab starts from the close button. Focus the primary button after the dialog body is inserted. Dialogs with a field to fill in (such as Delete User, which asks to type DELETE) are left as they are.
2026-09-30 20:43:45 +10:00
FreeScout
2f1739e817 Keep aspect ratio of images shrunk by max-width in conversation view #5687 from il2020/master
Keep aspect ratio of images shrunk by max-width in conversation view
2026-09-30 13:40:37 +03:00
il2020
1beaa0b60e Keep aspect ratio of images shrunk by max-width in conversation view 2026-09-30 14:28:59 +04:00
FreeScout
f46f1d678b Fit wide images in user email notifications #5686 from il2020/master
Fit wide images in user email notifications
2026-09-30 12:49:02 +03:00
FreeScout
c5d8acb937 Make icon buttons and menu toggles reachable and usable with the keyboard #5683 from JayOfTheKeyboard/fix/toolbar-keyboard-access
Make icon buttons and menu toggles reachable and usable with the keyboard
2026-09-30 12:37:39 +03:00
il2020
c96b24a7da Fit wide images in user email notifications 2026-09-30 13:36:18 +04:00
FreeScout
78e987c920 Adjust the time alignment in a user notification #5684 from il2020/master
Adjust the time alignment in a user notification
2026-09-30 11:54:24 +03:00
il2020
dffbf0edf2 adjust the time in a user notification 2026-09-30 10:11:18 +04:00
JayOfTheKeyboard
d661beeea2 Make icon buttons and menu toggles reachable and usable with the keyboard
The conversation toolbar icons (Reply, Note, Delete, More Actions), the star in the conversation and in the list, each thread's options menu and the mailbox and user sidebar menus are span/i elements with role="button" (or no role) and no tabindex, so Tab skips them and Enter or Space does nothing. Add tabindex="0" (only where the menu is rendered, so there are no empty Tab stops) and a delegated keydown handler that clicks role="button" spans and icons on Enter and Space, ignoring key repeat. Space on a dropdown toggle is left to Bootstrap, which already opens it.
2026-09-30 15:54:37 +10:00
FreeScout
929c575d28 Add To without space before < in WebklexHeaderTest - closes #5678 2026-09-29 05:57:23 -07:00
FreeScout
42d753acbb Add To without space before < in WebklexHeaderTest - closes #5678 2026-09-29 05:50:28 -07:00
FreeScout
56867a0b79 Fix parsing of addresses without a space between display name and < #5678 from il2020/master
Fix parsing of addresses without a space between display name and <
2026-09-29 15:45:57 +03:00
FreeScout
8d4eb9b899 Fix fetching several messages when one of them fails - closes #5668 2026-09-29 05:43:22 -07:00
FreeScout
be242eaa92 update.sh reports "latest version" when exactly one new commit is available #5679 from il2020/fix/update-sh-commit-count
update.sh reports "latest version" when exactly one new commit is available
2026-09-29 15:36:41 +03:00
FreeScout
11663cf58d Fix SsrfProtectionTest 2026-09-29 05:34:32 -07:00
il2020
24e2fcaf20 update.sh reports "latest version" when exactly one new commit is available 2026-09-29 12:23:41 +00:00
FreeScout
0f87c3e50f Improve sanitizing remote URLs - GHSA-qgjh-pmpj-85m6 2026-09-29 04:28:54 -07:00
il2020
a42a2a7134 Fix parsing of addresses without a space between display name and < 2026-09-29 11:23:44 +04:00
FreeScout
c224ec4ef5 Outlook conditional comments <![if !vml]> / <![endif]> shown as text in thread body #5676 from il2020/vml-fix
Outlook conditional comments <![if !vml]> / <![endif]> shown as text in thread body
2026-09-28 14:15:16 +03:00
FreeScout
7ccaee0518 Fix forwarding attachments with non-local storage - closes #5675 2026-09-28 03:57:43 -07:00
FreeScout
7e90331746 Fix restoring resipients after discarding a draft - closes #5674 2026-09-27 23:34:59 -07:00
il2020
86a120da0c Outlook conditional comments <![if !vml]> / <![endif]> shown as text in thread body 2026-09-28 10:31:12 +04:00
FreeScout
89bc9265b8 Fix restoring Cc and Bcc after Undoing sending - closes #5674 2026-09-27 23:17:07 -07:00
FreeScout
a287c272bf Style adjustments 2026-09-27 22:59:32 -07:00
FreeScout
d709f731a8 Style adjustments 2026-09-27 22:49:58 -07:00
FreeScout
57b8f79482 Fix Helper::getPrivateStorage() - closes #5675 2026-09-27 21:14:25 -07:00
FreeScout
684f81409a Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2026-09-27 03:41:36 -07:00
FreeScout
2dfa1fcb39 v1.8.243 2026-09-27 03:41:00 -07:00
FreeScout
1a9c5b8d08 Fix opening attachments - closes #5673 2026-09-27 03:36:02 -07:00
FreeScout
243320ddb9 Conversation list: fix viewer badge showing the current user, show all viewers 2026-09-26 23:45:04 -07:00
FreeScout
2265016523 Add error handling to Attachment::getFileContents() 2026-09-26 23:27:29 -07:00
FreeScout
1a1dbcc5e0 Conversation list: fix viewer badge showing the current user #5670 from il2020/master
Conversation list: fix viewer badge showing the current user
2026-09-27 07:45:57 +03:00
FreeScout
003d2070c6 Improve attachments size check - closes #5666 2026-09-26 03:41:30 -07:00
FreeScout
c422882ff8 Lowercase file name before comparing in OpenController - closes #5669 2026-09-26 03:04:23 -07:00
FreeScout
77175143d9 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2026-09-25 23:03:17 -07:00
FreeScout
3e194faa76 v1.8.242 2026-09-25 23:02:56 -07:00
FreeScout
17037f219d Fix failed() method in SendReplyToCustomer - closes #5671 2026-09-25 22:38:40 -07:00
FreeScout
8db5546bab Update readme 2026-09-25 11:22:20 -07:00
il2020
95c4acdd3a Conversation list: fix viewer badge showing the current user, show all viewers 2026-09-25 15:59:35 +04:00
FreeScout
1a49198844 Check attachment file existence in Attachment::duplicate() 2026-09-24 23:36:14 -07:00
FreeScout
8453429753 Add csrf token to Clone Conversation action - GHSA-qp58-4g6r-qq78 2026-09-24 21:46:25 -07:00
FreeScout
12adf2470c Make storage system compatible with remote storages - closes #5603 2026-09-23 21:53:38 -07:00
FreeScout
e3e8b521f1 Add testFromToCc() - closes #5659 2026-09-23 03:07:51 -07:00
FreeScout
46f1bc4658 Merge pull request #5659 from il2020/master
Strip RFC 5322 quotes from display names in `From`/`To`/`Cc`
2026-09-23 09:27:12 +03:00
il2020
24d0177cf8 Strip RFC 5322 quotes from display names in From/To/Cc 2026-09-23 09:40:56 +04:00
FreeScout
96523cf6d9 Allow dl, dt and dd to purifier config - closes #5654 2026-09-22 03:18:50 -07:00
FreeScout
2221777f3a Fix Attempt to read property id on null in Thread::getFromIfDifferentFromReplyTo() - closes #5655 2026-09-21 21:13:55 -07:00
FreeScout
05d45d3037 WebklexHeaderTest - closes #5653 2026-09-19 10:56:50 -07:00
FreeScout
fd24aa6dbd Fix extra space inserted into long customer names #5653 from il2020/master
Fix extra space inserted into long customer names
2026-09-19 20:18:16 +03:00
il2020
2f6b583bf1 Fix extra space inserted into long customer names 2026-09-19 19:30:34 +04:00
FreeScout
0489a6c29f Trim ending space from users without last name - closes #5651 2026-09-19 06:35:21 -07:00
FreeScout
7c64e53126 Fix default module icon when app is used in subfolder 2026-09-19 06:28:56 -07:00
FreeScout
dbbfff831f Fix Mailbox::excludeArchived() - closes #5647 2026-09-19 03:07:25 -07:00
FreeScout
88734eec10 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2026-09-18 21:16:11 -07:00
FreeScout
01f5a69e6a v1.8.241 2026-09-18 21:15:57 -07:00
FreeScout
e37596f1b6 Update German translation 2026-09-17 23:02:20 -07:00
FreeScout
c0248c2a8e Send UserEmailReplyError emails when user has no access to mailbox or conversation - closes #5643 2026-09-17 22:55:10 -07:00
FreeScout
6bc4005fa1 Fix Helper::stripTagsFromArray() - closes #5642 2026-09-17 21:58:46 -07:00
FreeScout
ef99594188 Fix changing conversation customer when APP_LIMIT_USER_CUSTOMER_VISIBILITY enabled - closes #5640 2026-09-16 07:40:25 -07:00
FreeScout
97a561e99e Auto link last entered link - closes #5280 2026-09-16 00:03:36 -07:00
FreeScout
504a7f71dc Remove HTML from port unavailable message 2026-09-15 23:02:22 -07:00
FreeScout
bd7fa38500 Implement client->getLastError() function for webklex/php-imap - closes #5633 2026-09-15 06:45:25 -07:00
FreeScout
29f057aa4c Fix fetching from Stalwart mail server - closes #5581 2026-09-15 06:09:44 -07:00
FreeScout
cd129aa634 Use caching for Conversation::userHasAccessToMailbox() and Conversation::isMailboxArchived() 2026-09-14 23:48:56 -07:00
FreeScout
198d839a4a Hide notifications text when user loses access to mailbox or conversation - GHSA-6545-8c3f-pp6w 2026-09-14 21:50:08 -07:00
FreeScout
945fb2f583 Adjust Modules page 2026-09-14 11:23:31 -07:00
FreeScout
7f977959df Retry "connection failed - SSL operation failed" error on fetching - closes #5623 2026-09-14 07:45:53 -07:00
FreeScout
e60e1cd3c3 Check user access to conversation in SendNotificationToUsers job - GHSA-2j2c-g32w-96vm 2026-09-14 04:46:01 -07:00
FreeScout
b00c3f5f5d Do not allow to save existing conversation as draft - GHSA-6ff4-3w2c-mjj8 2026-09-13 21:53:35 -07:00
FreeScout
ab1c70d413 Check if mailbox is active in Mailbox::userHasAccess() - GHSA-59v9-2qvg-cxw8 2026-09-13 21:18:05 -07:00
FreeScout
9c594f406b Do not show .env file content on final step of Installation Wizard - GHSA-3gv6-4vmm-79pj 2026-09-13 11:11:44 -07:00
FreeScout
5be2d61755 Fix "Show Original" on IMAP servers which reject searching by Message-ID 2026-09-13 03:48:50 -07:00
FreeScout
8ee81b6050 Merge pull request #5633 from il2020/master
Fix "Show Original" on IMAP servers which reject searching by Message-ID
2026-09-13 07:40:27 +03:00
il2020
e5d1065743 Fix "Show Original" on IMAP servers which reject searching by Message-ID 2026-09-12 22:25:46 +04:00
FreeScout
6fae3eecb1 Add Paste as Plain Text editor option - closes #5627 2026-09-12 11:20:09 -07:00
FreeScout
fe7d4711c0 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2026-09-11 22:58:18 -07:00
FreeScout
4be0be3982 v1.8.240 2026-09-11 22:58:04 -07:00
FreeScout
aada309876 Add testPlainBodyRead() test - closes #5624 2026-09-11 22:44:32 -07:00
FreeScout
3d5cffc50c Allow data: in frame-src CSP rule - closes #5630 2026-09-11 21:45:16 -07:00
FreeScout
f7a0b56db3 Use user timezone in text notifications - closes #5628 2026-09-11 04:04:06 -07:00
FreeScout
c61d5c97b7 Add America/Vancouver timezone - closes #5622 2026-09-08 19:21:28 -07:00
FreeScout
12fb019cac Use Message-Id from headers for In-Reply-To when message_id is generated - closes #5620 2026-09-07 21:43:00 -07:00
FreeScout
f41709a6cf Use Message-Id from headers for In-Reply-To when message_id is generated - closes #5620 2026-09-07 03:52:23 -07:00
FreeScout
8597a4ca52 Adjust replies counter color 2026-09-07 02:59:44 -07:00
FreeScout
af49c663e0 Add csrf token to Undo Reply action - GHSA-5vw8-4wxh-6mpr 2026-09-07 01:58:57 -07:00
FreeScout
e91f2e410c Update SECURITY.md 2026-09-07 01:27:41 -07:00
FreeScout
d9d02b5258 Add Helper::floatingFlash() function 2026-09-07 00:33:33 -07:00
FreeScout
b386b9034f Automatically rename incrorrectly names Module folders on activation 2026-09-06 03:55:37 -07:00
FreeScout
8667724e0b Add Azerbaijani translation 2026-09-06 00:38:00 -07:00
FreeScout
0567e3906b Merge pull request #5619 from jamalkamaladdin/feat/az-locale
Add Azerbaijani translation
2026-09-06 10:21:22 +03:00
FreeScout
d3034ab4f3 Allow to Archive mailboxes 2026-09-06 00:18:18 -07:00
Jamal
8911f03f50 Add Azerbaijani translation 2026-09-05 16:17:00 +04:00
FreeScout
7c998efe8e Highlight inactive mailboxes in UI 2026-09-05 01:43:37 -07:00
FreeScout
5286397847 Extend mailbox.get_mail_from_name hook parameters 2026-09-04 08:03:57 -07:00
FreeScout
053fff2634 Do not send email to customer via SendReplyToCustomer listener if thread is not set in event - closes #5616 2026-09-04 05:21:02 -07:00
FreeScout
609d065b05 Highlight number of replies in conversations list 2026-09-04 04:01:31 -07:00
FreeScout
730401e757 Highlight number of replies in conversations list 2026-09-04 03:30:22 -07:00
FreeScout
3d01c23f0b Update Readme 2026-09-04 01:37:36 -07:00
FreeScout
6420feed47 Fix To field in notes created when forwarding to multiple recipients - closes #5441 2026-09-04 00:54:22 -07:00
FreeScout
c1bc7b90ef Update Modules page menu 2026-09-03 23:53:59 -07:00
FreeScout
ce56346290 Update Readme 2026-09-03 23:16:00 -07:00
FreeScout
3b471b17cf Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2026-09-03 22:13:26 -07:00
FreeScout
34537a1834 v1.8.239 2026-09-03 22:13:06 -07:00
FreeScout
8293c064d9 Keep CRLF header separators in MailTransport so mail() output stays valid MIME - closes #5615 2026-09-03 10:28:09 -07:00
FreeScout
1d115c6be8 Improve htmlEscape() in main.js 2026-09-03 09:55:13 -07:00
FreeScout
2b71bbadbb Update readme 2026-09-03 07:04:48 -07:00
FreeScout
ad8f305b13 Merge pull request #5611 from jdevalk/fix/preserve-css-custom-properties
Preserve native CSS custom properties during minification
2026-09-03 11:49:29 +03:00
Joost de Valk
09a18e6d0f Fix preserving CSS custom properties during minification 2026-09-03 07:15:21 +02:00
FreeScout
a3cacca368 Drop the dead $uri self-assignment in the laroute overridet #5609 from darkdi/drop-dead-uri-assignment
Drop the dead $uri self-assignment in the laroute override
2026-09-02 19:43:55 +03:00
Dmitry Rantovov
c1d63c7549 Drop the dead $uri self-assignment in the laroute override 2026-09-02 18:53:53 +03:00
FreeScout
b792e25eec Take into account IPv6 transition addresses in Helper::sanitizeRemoteUrl() - GHSA-v5xm-qw3f-qm98 2026-09-01 22:53:04 -07:00
FreeScout
c5b8917747 Extend Thread-Index with a child block on replies so Outlook groups them from alamedaim-apps/outlook-thread-index-child-block
Extend Thread-Index with a child block on replies so Outlook groups them (#4922)
2026-09-02 08:15:47 +03:00
Ross Taylor
891c971bb9 Extend Thread-Index with a child block on replies so Outlook groups them (#4922)
Outlook groups conversations by Thread-Index and expects a reply to extend the
parent's index with a 5-byte child block (MS-OXOMSG 2.2.1.3). Echoing the
customer's index verbatim makes Outlook open a new conversation, leaving the
customer's original message on its own. Only affects replies to mail that
already carried Thread-Index; other senders are unchanged.
2026-09-02 01:48:48 +00:00
FreeScout
31a9856819 Disable support of attachment URLs with MD5 tokens - GHSA-89ww-mfww-5vr6 2026-09-01 09:00:19 -07:00
FreeScout
723598aa27 Allow to set modal class via parameters 2026-09-01 00:42:20 -07:00
FreeScout
188a738528 Helper::getAppLocale() function 2026-08-31 03:24:05 -07:00
FreeScout
5f140b117f Remove black outline when clicking on links in Chrome 2026-08-31 01:00:12 -07:00
FreeScout
4080415870 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2026-08-30 22:48:38 -07:00
FreeScout
9e1ca45169 v1.8.238 2026-08-30 22:48:14 -07:00
FreeScout
5e32a629a3 Improve populating conversation_id field in add_conversation_id_column_to_notifications_table migration 2026-08-30 22:33:22 -07:00
FreeScout
7390179434 Escape message in showFloatingAlert() 2026-08-30 04:13:42 -07:00
FreeScout
95513f1832 Update PHP versions in GitHub tests 2026-08-29 21:35:58 -07:00
FreeScout
ed1a7ca9e3 Allow to change default modal loader 2026-08-29 21:34:25 -07:00
FreeScout
fa86462665 Adjust .note-actions style 2026-08-29 05:05:06 -07:00
FreeScout
d8f21f070b Allow to configure fetching max execution time via APP_FETCH_MAX_EXECUTION_TIME #5605 from alanost/configurable-fetch-max-execution-time
Allow to configure fetching max execution time via APP_FETCH_MAX_EXECUTION_TIME
2026-08-29 14:32:13 +03:00
Alan
e52e641636 Allow to configure fetching max execution time via APP_FETCH_MAX_EXECUTION_TIME 2026-08-29 11:14:28 +00:00
FreeScout
4b17e534a7 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2026-08-29 03:00:55 -07:00
FreeScout
90209e8b54 v1.8.237 2026-08-29 03:00:39 -07:00
FreeScout
c8f4e43aef Change Show Original link case in thread 2026-08-28 07:21:43 -07:00
FreeScout
721bc200aa Update Finnish translation 2026-08-28 07:17:41 -07:00
FreeScout
46bd96f442 Mark notifications as read by convesrsation_id field in notifications table - closes #5600 2026-08-28 04:14:49 -07:00
FreeScout
51543a4be5 Adjust GenerateVars console message 2026-08-28 02:56:45 -07:00
FreeScout
d416863d20 Store vars.js in /js/builds 2026-08-28 01:49:40 -07:00
FreeScout
9dc9053f66 Update PULL_REQUEST_TEMPLATE 2026-08-28 01:16:26 -07:00
FreeScout
89fced362e Add Strict-Transport-Security header 2026-08-28 01:15:53 -07:00
FreeScout
1e886cbff1 Show From when thread's customer differs from conversation's customer - closes #5601 2026-08-27 22:59:13 -07:00
FreeScout
a381fe69ec Show log name in title 2026-08-27 04:34:54 -07:00
FreeScout
78cf02d983 Adjust events parameters 2026-08-27 04:24:11 -07:00
FreeScout
bce23ef2d3 Show action name for TriggerAction jobs in System Status 2026-08-27 04:23:38 -07:00
FreeScout
868061500a Add Strict-Transport-Security header 2026-08-27 02:12:00 -07:00
FreeScout
69dd7a98dd Add X-Content-Type-Options nosniff header 2026-08-27 01:47:26 -07:00
FreeScout
bd54392ca4 Improve audio downloading/playing - closes #5597 2026-08-27 00:13:58 -07:00
FreeScout
4e8e4d80c7 Do not allow unsafe symlinks when UnZipping - GHSA-rw7g-qq32-c669 2026-08-26 02:51:43 -07:00
FreeScout
06e0e85dcc Add comment for APP_LIMIT_USER_CUSTOMER_VISIBILITY parameter 2026-08-26 02:08:57 -07:00
FreeScout
ddbf13b0b5 Fix user notifications when conversation is moved - closes #5596 2026-08-25 09:22:10 -07:00
FreeScout
1cc09c8a6e Require PHP 7.4+ - closes #5595 2026-08-25 06:55:26 -07:00
FreeScout
c0fde64b42 Allow to fetch emails having same Message-IDs - closes #5591 2026-08-25 06:51:48 -07:00
FreeScout
c8b20fbe4a Update Readme 2026-08-25 02:28:00 -07:00
FreeScout
adde156bc2 Update Readme 2026-08-25 02:25:01 -07:00
FreeScout
c1839cca8a Helper::curlClose() 2026-08-25 02:02:42 -07:00
FreeScout
1aa8ffa9cf Adjust design elements - closes #5592 2026-08-24 22:11:42 -07:00
FreeScout
27f752d23f HTML escape folder names in Get Folders 2026-08-24 07:58:55 -07:00
FreeScout
33661df11a Allow to fetch emails having same Message-IDs - closes #5591 2026-08-24 07:51:40 -07:00
FreeScout
f87c43522d Correctly process customer reply to a previous agent reply after moving conversation - closes #5590 2026-08-24 02:15:14 -07:00
FreeScout
1040e0af3c Adjust error message check in FetchEmail for increasing sleep time - closes #5584 2026-08-24 00:28:00 -07:00
FreeScout
fcfb029710 Improve OAuth IMAP token refresh logic to avoid errors in logs every hour - closes #5582 2026-08-24 00:24:36 -07:00
FreeScout
0e573cd6f9 Catalan: use imperative forms per Softcatalà style guide #5589 from losimo/fix/catalan-imperative-forms
Catalan: use imperative forms per Softcatalà style guide
2026-08-24 10:10:25 +03:00
FreeScout
6c2bf1d59b Check authentication in /polycast/connect and require channels parameter in /polycast/receve - GHSA-7x4c-p73f-67x5 2026-08-23 23:55:33 -07:00
FreeScout
a9572ca50a Add vendor files to dist 2026-08-23 22:28:51 -07:00
FreeScout
63b008c6d2 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2026-08-23 22:28:24 -07:00
FreeScout
e42c59ab88 v1.8.236 2026-08-23 22:28:11 -07:00
Albert Simó
58a49aa806 Catalan: use imperative forms per Softcatalà style guide
The Catalan translation used infinitives for buttons and menu items
("Desar", "Editar", "Eliminar"). The Softcatalà style guide, the
reference for Catalan software localisation, asks for the second
person singular imperative when the user commands the application:
"Desa", "Edita", "Elimina". Confirmation dialogs are the opposite
case, the application addressing the user, so those now read
"Voleu eliminar aquesta conversa?" rather than an imperative.

Also translates strings added in recent releases that had no Catalan
yet, among them the autoplay notice from the chat sound notifications
in 1.8.233, the fetch error threshold settings in 1.8.235 and the
password reset message that no longer reveals whether an account
exists.

159 corrected entries, 128 added.
2026-08-24 00:05:48 +02:00
FreeScout
0f47724cb7 Check access to mailboxes when non-admin creates users - GHSA-w36p-9vhj-x273 2026-08-21 00:43:34 -07:00
FreeScout
38b9a236a8 Do not allow users who can see only assigned conversations delete any conversations in folder via empty_folder - GHSA-6mhr-m8m9-6q6h 2026-08-20 12:05:42 -07:00
FreeScout
c04444bfbf Check folder type in empty_folder action 2026-08-20 11:31:01 -07:00
FreeScout
bbb8f94417 Remove commit from setReplyBody() 2026-08-20 07:45:56 -07:00
FreeScout
198a2afe75 Adjust error message check in FetchEmail for increasing sleep time - closes #5584 2026-08-20 06:14:47 -07:00
FreeScout
002740b267 Trigger user notifications when moving conversations - closes #5580 2026-08-20 02:50:36 -07:00
FreeScout
ef10136cb8 Enlarge dropdown caret element 2026-08-20 02:41:49 -07:00
FreeScout
343a21e457 Allow text/rfc822-headers content type when processing incoming bounce messages - closes #5583 2026-08-20 02:20:40 -07:00
FreeScout
f706284996 Store conversation number in log when sending error occurs - #5571 2026-08-19 22:59:33 -07:00
FreeScout
b5c737cdf8 Update SECURITY.md 2026-08-18 03:55:47 -07:00
FreeScout
26ac7e2c62 Apply module migrations when the Modules folder resolves outside the app folder #5574 from RhinoBag/fix/5569-module-migrate-path
Apply module migrations when the Modules folder resolves outside the app folder
2026-08-17 20:53:32 +03:00
Jörn Bungartz
34d2a019b0 Apply module migrations when the Modules folder is outside the app folder
module:migrate hands the module's migrations folder to the migrate command
after removing base_path() from it. Module::getPath() is a realpath(), so on
installations where the Modules folder is a symlink leading outside of the
application folder (a common container layout: /var/www/html/Modules ->
/data/Modules) base_path() does not occur in that path. The value stays
absolute, migrate prepends base_path() to it anyway (BaseCommand::
getMigrationPaths()), the resulting folder does not exist, and the migration
reports "Nothing to migrate" with exit code 0.

Updating a module through the Modules page then leaves the old schema in
place, because Module::updateModule() relies on freescout:module-install and
nothing reports the failure. Activating a module is not affected, as
ModulesController runs a plain migrate afterwards.

Modules now remember the path they have been scanned from, which still
contains the symlink and is therefore relative to base_path(), and the
module:migrate command builds the --path option from it. The scanned path
also travels through the modules cache, as Collection::toArray() keeps the
resolved path only. When neither path is inside the application folder the
command reports it instead of reporting success.

closes #5569
2026-08-17 10:39:21 -06:00
FreeScout
661c000a0e Fix CTRL+ keyboard shortcuts on Mac - closes #5572 2026-08-17 01:34:41 -07:00
FreeScout
e2a5930a5a Remove temporary SwiftMailer files in freescout:clean-tmp command - closes #5558 2026-08-17 00:16:47 -07:00
FreeScout
7fad593186 JS conversation.show_reply_form hook 2026-08-16 02:54:50 -07:00
FreeScout
752ce0f46b Highlight sender in thread when From is different from Reply-To 2026-08-15 05:30:10 -07:00
FreeScout
980c6533a8 PHP 8.5 compatibility - closes #5568,#5567 2026-08-15 03:24:53 -07:00
FreeScout
9dc66919e1 Highlight sender in thread when From is different from Reply-To 2026-08-15 03:06:31 -07:00
FreeScout
68ba521efa Remove temporary SwiftMailer files in freescout:clean-tmp command - closes #5558 2026-08-15 02:53:09 -07:00
FreeScout
ab27725368 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2026-08-15 02:35:45 -07:00
FreeScout
84bf959276 v1.8.235 2026-08-15 02:35:27 -07:00
FreeScout
3d14b81971 Remove folder_id_state index from Conversations 2026-08-15 00:39:16 -07:00
FreeScout
d3bb99dbc1 Fix actor in web notifications - closes #5566 2026-08-14 10:45:40 -07:00
FreeScout
1407a1bfdd Check user access to conversation when processing reply to email notification - GHSA-83xx-8c27-gq78 2026-08-14 10:05:44 -07:00
FreeScout
0f41f5cabb Sanitize HTTP_CF_CONNECTING_IP in Request and escape values in Logs Monitor email - GHSA-9cm3-qvj2-8hg4 2026-08-14 07:42:13 -07:00
FreeScout
21768e1090 Perform user->canSeeOnlyAssignedConversations() check in ConversationsController->conversationsFilterQuery() and Conversation::getChats() - GHSA-c83g-9f2m-9cmw 2026-08-14 07:27:36 -07:00
FreeScout
db4b4e4b4a Check checkIsOnlyAssigned() when editing or deleting user threads - GHSA-89fj-jcr7-2c68 2026-08-14 06:50:29 -07:00
FreeScout
2b3d120a49 Improve preserving spaces in the conversation preview 2026-08-14 05:07:25 -07:00
FreeScout
c9347ee8a9 Add Mailbox::sanitizeSignature() function 2026-08-14 04:47:49 -07:00
FreeScout
47c98e97a4 Fix variables in mailbox signature - closes #5565 2026-08-14 04:40:37 -07:00
FreeScout
f9c8089ed5 Fix sequence parameter in Webklex Message 2026-08-14 04:08:23 -07:00
FreeScout
9bd8a6527c Fix icon tags in Summernote - closes #5564 2026-08-14 02:37:42 -07:00
FreeScout
ec0faecf7f Add vendor files to dist 2026-08-14 02:04:13 -07:00
FreeScout
71e77d7b5a Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2026-08-14 02:03:40 -07:00
FreeScout
a49b61d693 v1.8.234 2026-08-14 02:03:25 -07:00
FreeScout
10e8d0c867 Allow CODE and KBD tags in Purifier config - closes #5556 2026-08-14 01:24:33 -07:00
FreeScout
ae55bd7a36 Check curl HTTP code in Helper::getRemoteFileContents() - closes #5552 2026-08-14 01:09:17 -07:00
FreeScout
3101b08d8b Fix path to PostTooLargeException - closes #5560 2026-08-14 00:44:43 -07:00
FreeScout
8d41a382f2 Helper::hashEquals() instead of hash_equals() 2026-08-14 00:37:35 -07:00
FreeScout
8a8dd09a0c Fix conversation ID in BroadcastNotification::fetchPayloadData() - closes #5553 2026-08-14 00:18:13 -07:00
FreeScout
d0ed4fda50 Allow to extend statuses 2026-08-14 00:13:05 -07:00
FreeScout
6e84ce7840 Allow to extend statuses 2026-08-11 20:01:05 -07:00
FreeScout
61c6fe0ec6 Update Readme 2026-08-07 05:49:43 -07:00
FreeScout
e67f09fe7e Add hooks to reply_fancy template 2026-08-07 05:49:06 -07:00
FreeScout
9492779dfc Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2026-08-06 21:22:21 -07:00
FreeScout
499fcfc6ca v1.8.233 2026-08-06 21:20:52 -07:00
FreeScout
369125c24f Add Helper::carbonDateParse() function 2026-08-06 10:50:40 -07:00
FreeScout
14faec272c Allow style attribute in inline elements in Purifier config - closes #5547 2026-08-06 09:42:35 -07:00
FreeScout
5c55597650 Remove Outlook reply header from replies to email notifications - fixes #5545 from RhinoBag/fix-5545-notification-reply-outlook-header
Remove Outlook reply header from replies to email notifications - fixes #5545
2026-08-06 07:47:34 +03:00
Jörn Bungartz
24e0a9a01b Remove Outlook reply header from replies to email notifications - fixes #5545 2026-08-05 11:47:51 -06:00
FreeScout
8ea4963b75 Fix Outlook reply separator - closes #5544 2026-08-05 10:34:48 -07:00
FreeScout
73832baf03 Update Readme 2026-08-04 09:40:22 -07:00
FreeScout
fab578bb5c Add WAITING_SINCE_AS_FIRST_UNANSWERED_CUSTOMER_MESSAGE .env parameter - closes #5225 2026-08-04 09:36:44 -07:00
FreeScout
ea2820d04a Fix too long References header in emails to customers - closes #5542 2026-08-03 00:06:13 -07:00
FreeScout
c333ea2780 Create forward-note for each recipient when forwarding conversation to multiple recipients - closes #5441 2026-08-02 04:27:14 -07:00
FreeScout
ddadec3c2a Do not insert HR tag on CTRL+ENTER click - closes #4909 2026-08-02 01:55:05 -07:00
FreeScout
f0533a5470 Add commit() method to Summernote to fix CTRL+Z after loading draft - closes #4837 2026-08-02 01:28:26 -07:00
FreeScout
482b002a8e Update Readme 2026-08-01 05:15:32 -07:00
FreeScout
806f3d0228 Update Readme 2026-08-01 04:58:18 -07:00
FreeScout
2e83003030 Add Conversation::setLastReplyAt() function 2026-08-01 01:26:53 -07:00
FreeScout
756549c157 Add WAITING_SINCE_AS_FIRST_UNANSWERED_CUSTOMER_MESSAGE .env parameter - closes #5225 2026-08-01 00:37:14 -07:00
FreeScout
95fa038205 Audio notifications for chat messages 2026-07-31 10:41:27 -07:00
FreeScout
e081807436 Add vendor files to dist 2026-07-31 01:11:56 -07:00
FreeScout
49f52f7fd9 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2026-07-31 01:06:11 -07:00
FreeScout
7c4539d445 v1.8.232 2026-07-31 01:05:47 -07:00
FreeScout
90354deb6c Add functions to required functions 2026-07-30 07:29:20 -07:00
FreeScout
2e7ad9a234 Improve Helper::checkUrlIpAndHost() function 2026-07-30 05:38:08 -07:00
FreeScout
9ad3a8403a Improve Helper::checkUrlIpAndHost() function for IPv6 addresses 2026-07-30 00:42:22 -07:00
FreeScout
6141a718c4 Fix messages sorting on fetching - closes #5536 2026-07-29 21:57:27 -07:00
FreeScout
7efcfd9809 Add Helper::sanitizeEmail() function 2026-07-29 01:15:37 -07:00
FreeScout
de383fd9b6 Adapted instructions to recommend setting PHP_PATH in tools.php - closes #5534 2026-07-28 23:14:11 -07:00
FreeScout
bdef99d68e Enable EscapeNonASCIICharacters option only for outgoing replies - closes #5481 2026-07-28 23:04:50 -07:00
FreeScout
403f02f613 Allow to send just image 2026-07-28 07:09:07 -07:00
FreeScout
0f40c152e3 Do not allow regular users with PERM_EDIT_USERS edit admin profiles 2026-07-28 03:28:49 -07:00
FreeScout
d9b96b7a28 Fix case of items in Helper::checkUrlIpAndHost() 2026-07-28 02:13:41 -07:00
FreeScout
f4a84f47e3 Sanitize URL in Customer::setPhotoFromRemoteFile() 2026-07-28 02:12:19 -07:00
FreeScout
2c3d484271 White background for logo-300.png 2026-07-28 01:56:38 -07:00
FreeScout
0d9a508f2c Upgrade ezyang/htmlpurifier library - closes #5529 2026-07-27 06:22:57 -07:00
FreeScout
441e46d65d Update Guzzle to the latest version 2026-07-27 02:25:14 -07:00
FreeScout
582961ec86 guzzlehttp/psr7: Host Confusion via Weak URI Host Validation - GHSA-c2w2-prh8-qm98 2026-07-27 00:38:35 -07:00
FreeScout
31d0e4199f Add Customer::getEmailsAsArray() method 2026-07-27 00:29:11 -07:00
FreeScout
2429b49fdc Add comment to ajaxHtml() action in SystemController 2026-07-26 23:28:29 -07:00
FreeScout
fc89e7df5e Revert changing last_reply_at field update logic - closes #5501 2026-07-26 10:23:00 -07:00
FreeScout
d2e42a91a2 Use hash_equals() to compare hashes 2026-07-26 00:24:38 -07:00
FreeScout
18b74a92f7 Remove non-used actions from CustomersController 2026-07-26 00:14:36 -07:00
FreeScout
b21600e8f9 Fix potential TypeErrors 2026-07-25 23:15:24 -07:00
FreeScout
3eb73bf375 Use hash_equals() when checking attachment token 2026-07-25 22:59:54 -07:00
FreeScout
069b76adaf Add hash to lang.zip name when downloading translations 2026-07-25 22:54:43 -07:00
FreeScout
2a14a079b7 Sanitize module alias on activating license 2026-07-25 22:40:24 -07:00
FreeScout
d74818bbf3 Helper::decryptSoft() 2026-07-25 22:26:12 -07:00
FreeScout
e6e13e20bd Restrict file extensions uploaded by support agents 2026-07-25 21:43:42 -07:00
FreeScout
caafa618db Purify mailbox signature HTML on saving 2026-07-25 09:53:49 -07:00
FreeScout
12863e7ad2 Escape title in symfony ExceptionHandler 2026-07-25 09:39:18 -07:00
FreeScout
85071db541 Throttle ResetPasswordController controller 2026-07-25 08:54:21 -07:00
FreeScout
de967ed952 Tighten throttling Forgot Password requests - GHSA-jvmv-2qcp-7855 2026-07-25 08:18:27 -07:00
FreeScout
a7e85bfcc4 Return empty value in Helper::decrypt() when decryption fails 2026-07-25 06:08:38 -07:00
FreeScout
11f4ed76b3 Use salt in tools.php auth 2026-07-25 05:00:41 -07:00
FreeScout
1c6bd51ef4 Restrict file extensions uploaded by support agents 2026-07-25 04:36:33 -07:00
FreeScout
196f3663d5 Add vendor files to dist 2026-07-25 03:29:27 -07:00
FreeScout
e08cdd5288 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2026-07-25 03:26:17 -07:00
FreeScout
16d392fb3c v1.8.231 2026-07-25 03:26:06 -07:00
FreeScout
b3a47afa77 Add comment on SVG sanitizing 2026-07-25 00:45:10 -07:00
FreeScout
2ee52deaa1 Disable /update/ routes 2026-07-25 00:30:28 -07:00
FreeScout
acdc9ef5b5 Build Zipball URL from config in Self-updater 2026-07-24 23:07:45 -07:00
FreeScout
ad8f668998 Respect CURLOPT_SSL_VERIFYPEER config parameter in Helper::curlGetNextRedirectedUrl() 2026-07-24 22:23:36 -07:00
FreeScout
970ae6e2ba Do not open redirected URL in Helper::sanitizeRemoteUrl() 2026-07-24 22:19:26 -07:00
FreeScout
979e34dd5c Improve Helper::checkUrlIpAndHost() function for IPv6 addresses 2026-07-24 21:56:57 -07:00
FreeScout
e1f103568d Tighten throttling Forgot Password requests - GHSA-jvmv-2qcp-7855 2026-07-24 21:22:59 -07:00
FreeScout
e6721a094b Make Shebang generic in update.sh and install.sh - closes #5521 2026-07-23 01:48:14 -07:00
FreeScout
5fe3c3838f Show warning on Status page when HTTPS is used but SESSION_SECURE_COOKIE=true is not set 2026-07-23 01:29:29 -07:00
FreeScout
200a2b38ff Adjust bootstrap 2026-07-23 01:00:59 -07:00
FreeScout
1e970caffa Fix auto-focusing the reply area when replying 2026-07-22 23:25:05 -07:00
FreeScout
b12bb42d47 Fix missing space in browser push notifications - closes #5520 2026-07-22 23:17:47 -07:00
FreeScout
72bd06f6d2 Create new conversation for email notifications forwarded to another mailbox - closes #4515 2026-07-22 23:13:49 -07:00
FreeScout
9d16a0d4d3 Create new conversation for email notifications forwarded to another mailbox - closes #4515 2026-07-22 21:50:43 -07:00
FreeScout
e1b4a22383 Fix auto-focusing the reply area when replying 2026-07-21 23:18:53 -07:00
FreeScout
059301b720 Update German translation 2026-07-20 22:40:37 -07:00
FreeScout
6e1b61fbb9 Do not allow to send reply till all uploads finished - closes #5515 2026-07-20 09:24:54 -07:00
FreeScout
24fdc40dc9 PHP 8.5 compatibility - closes #5516 2026-07-20 09:09:08 -07:00
FreeScout
95fcc75f90 Update example in tools.php - closes #5507 2026-07-20 03:59:54 -07:00
FreeScout
646e8266d8 Update customer in threads when merging customers - closes #5505 2026-07-19 07:36:00 -07:00
FreeScout
2fff439530 Allow to define PHP path via PHP_PATH env parameter for tools.php script - closes #5507 2026-07-19 07:09:53 -07:00
FreeScout
8f2a7f2703 Change customer when reply to conversation from completely new email is received - GHSA-j49f-9g94-3wh4 2026-07-19 01:29:10 -07:00
FreeScout
83030464ee Restrict access to Assigned Conversations option in user profile - GHSA-m4hc-rc98-38jc 2026-07-18 23:41:41 -07:00
FreeScout
f0f8fd5625 Check customer visibility in load_customer_info ajax - GHSA-cr68-27qv-p5m4 2026-07-18 23:27:12 -07:00
FreeScout
5ce756de00 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2026-07-18 09:25:32 -07:00
FreeScout
609f7be8d2 v1.8.230 2026-07-18 09:23:16 -07:00
FreeScout
1b50b24c62 Fix separating replies from Outlook - closes #5504 2026-07-16 09:50:56 -07:00
FreeScout
41fd15bb68 Improve showing bell notifications - closes #5491 2026-07-14 07:16:10 -07:00
FreeScout
6a45fff348 Fix real time web notifications - closes #5499 2026-07-13 21:33:24 -07:00
FreeScout
3f508d41a0 Send CSP header in /ajax-html/ URLs - GHSA-2g42-f97q-973x 2026-07-11 11:20:22 -07:00
FreeScout
a4c587cf1f Improve remote URL sanitizing - GHSA-6w8v-qp43-vg2h 2026-07-11 11:17:52 -07:00
FreeScout
09799fd4bc Improve remote URL sanitizing - GHSA-6w8v-qp43-vg2h 2026-07-11 04:23:01 -07:00
FreeScout
154b513af8 Enable Core.EscapeNonASCIICharacters in Purifier - closes #5481 2026-07-08 01:53:52 -07:00
FreeScout
2f6fbd76dd Save conversation subject when pressing Enter in the subject fieldt #5492 from RhinoBag/fix-subject-save-on-enter
Save conversation subject when pressing Enter in the subject field
2026-07-07 08:47:40 +03:00
FreeScout
39bc5401ef Improve showing bell notifications - closes #5491 2026-07-06 22:43:46 -07:00
Jörn Bungartz
c98b075fca Save conversation subject when pressing Enter in the subject field
Previously the inline subject editor could only be saved by clicking the
check button. Pressing Enter did nothing (the input is not inside a form),
so users frequently thought the new subject was saved when it was not,
leading to replies going out with the old subject.

Bind Enter on #conv-subj-value to trigger the existing save button, matching
the Enter-to-submit behaviour already used for the reply body.
2026-07-06 14:20:04 -06:00
FreeScout
5a34f2a312 Do not send notifications to users when customer replies to Spam conversation 2026-07-05 04:52:59 -07:00
FreeScout
8cf431fc0f Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2026-07-04 07:46:42 -07:00
FreeScout
f454d3f445 v1.8.229 2026-07-04 07:46:29 -07:00
FreeScout
72a8137b03 Fix Undefined variable $addressString - closes #5487 2026-07-04 07:42:05 -07:00
FreeScout
3e577e82ca Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2026-07-04 07:30:23 -07:00
FreeScout
b13ec05c2a v1.8.228 2026-07-04 07:30:04 -07:00
FreeScout
9406e102ef Fix Undefined variable $ensureAddressesAreSafe - closes #5487 2026-07-04 07:29:15 -07:00
FreeScout
41b4441ab5 Adjust Modules page 2026-07-04 07:25:41 -07:00
FreeScout
85a5dbd38e Fix Undefined array key 10004 on outbound Guzzle request without a proxy - closes #5486 2026-07-04 07:04:31 -07:00
FreeScout
bf6d0abf5c Add vendor files to dist 2026-07-04 02:55:38 -07:00
FreeScout
e02b943b3f Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2026-07-04 02:54:41 -07:00
FreeScout
1704473a77 v1.8.227 2026-07-04 02:53:57 -07:00
FreeScout
f4dd553530 Fix proxiying requests with Guzzle 2026-07-04 02:50:42 -07:00
FreeScout
084cb6981b laravel/framework: Validate against line breaks in emails - GHSA-5vg9-5847-vvmq 2026-07-03 23:26:34 -07:00
FreeScout
06fa6b2940 Show proper error message when deactivating invalid license key 2026-07-03 11:39:06 -07:00
FreeScout
5f2925efd0 symfony/routing: Fix dot-segment encoding for chained "../" and "./" in generated URLs - GHSA-h5x3-xfc9-m39h 2026-07-03 11:35:00 -07:00
FreeScout
385a55b4b7 guzzlehttp/guzzle: Reject HTTPS proxies when libcurl lacks HTTPS-proxy suppor - GHSA-wpwq-4j6v-78m3 2026-07-03 11:26:35 -07:00
FreeScout
637fce7dbe guzzlehttp/guzzle: Reject HTTPS proxies when libcurl lacks HTTPS-proxy suppor - GHSA-wpwq-4j6v-78m3 2026-07-03 11:25:21 -07:00
FreeScout
e188765c21 guzzlehttp/guzzle: Reject dot-only cookie domains - GHSA-cwxw-98qj-8qjx 2026-07-03 11:00:39 -07:00
FreeScout
5b5ce88a90 guzzlehttp/psr7: Reject malformed Host values when constructing request URIs - GHSA-34xg-wgjx-8xph 2026-07-02 10:50:04 -07:00
FreeScout
7337879a65 guzzlehttp/psr7: Reject control and whitespace characters in URI host components - GHSA-hq7v-mx3g-29hw 2026-07-02 07:39:27 -07:00
FreeScout
adec0de39a guzzlehttp/psr7: Mitigate CRLF Injection in HTTP Start-Line Serialization - GHSA-vm85-hxw5-5432 2026-07-02 06:57:55 -07:00
FreeScout
7c485c96be Fix permissions check when merging conversations - GHSA-v8vc-cmf9-gg2c 2026-06-28 02:46:54 -07:00
FreeScout
779adccea0 Add vendor files to dist 2026-06-27 02:42:32 -07:00
FreeScout
62465bce79 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2026-06-27 02:41:41 -07:00
FreeScout
15c5aaaa15 v1.8.226 2026-06-27 02:40:52 -07:00
FreeScout
29c6c2884a Fix @fwd feature for Outlook emails - closes #5480 2026-06-25 23:29:38 -07:00
FreeScout
30ba029242 Fix linkifying URLs ending with > symbol - closes #5423 2026-06-25 03:48:38 -07:00
FreeScout
00f50ac6ca Allow to create users without last name - closes #4184 2026-06-24 23:49:09 -07:00
FreeScout
e7008fa267 On fetching preserve top-posted reply content before nested <html> block (Yahoo/Android) - closes #5409 2026-06-24 23:28:55 -07:00
FreeScout
460bc8f91b Send reply on CMD+Enter - closes #5476 2026-06-24 11:08:24 -07:00
FreeScout
caa5aac1c3 Fix: customer Waiting Since shows time of last customer message instead of first - closes #5475 2026-06-24 10:43:27 -07:00
FreeScout
4e197d8ce9 Check access permissions when searching conversations by number - GHSA-wqq7-4q7m-273v 2026-06-24 03:38:12 -07:00
FreeScout
b7082a8867 Prevent sending blank email when Summernote leaves empty HTML tags - closes #5478 2026-06-24 03:30:32 -07:00
FreeScout
e47a54a5af Improve RealtimeConvNewThread event access check - GHSA-w668-wq26-6c94 2026-06-22 22:43:21 -07:00
FreeScout
1f4deee636 Fix access check in realtime polycast events - GHSA-gh3r-jh6q-wrvj 2026-06-19 10:56:40 -07:00
FreeScout
656cb2349c Escape channel name in LIKE query in PolycastServiceProvider.php - GHSA-gh3r-jh6q-wrvj 2026-06-18 22:34:03 -07:00
FreeScout
38d49720e3 Fix Could not scan for classes error - closes #5469 2026-06-17 21:52:59 -07:00
FreeScout
9ad0db4947 Fix 500 in ConversationsController::view when the user has no Mine folder - closes #5466 2026-06-15 21:24:58 -07:00
FreeScout
5fe3f44f09 Fix json_decode() error in User::hasManageMailboxPermission() - closes #5465 2026-06-15 21:20:15 -07:00
FreeScout
73a7e1f9db Fetching: paginate by SEARCH count so the newest emails are no longer dropped - closes #5462 2026-06-12 22:00:45 -07:00
FreeScout
74fa4b7d4f Add vendor files to dist 2026-06-12 21:52:34 -07:00
FreeScout
899548fafa Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2026-06-12 21:52:01 -07:00
FreeScout
193adb7e11 v1.8.225 2026-06-12 21:51:48 -07:00
FreeScout
3d593022fe Update SECURITY.md 2026-06-11 22:44:41 -07:00
FreeScout
7f6a5042c1 Fix saving mailbox signature by non-admin users - closes #5443 2026-06-11 21:58:58 -07:00
FreeScout
315ad05f73 Improve Helper::stripDangerousTags() to strip nested tags - GHSA-jpq8-j69f-mj98 2026-06-08 21:56:44 -07:00
FreeScout
3193add63e Move option to UI: User can see only assigned conversations - closes #701 2026-06-06 20:58:51 -07:00
FreeScout
4503d750cb Move option to UI: User can see only assigned conversations - closes #701 2026-06-06 20:57:37 -07:00
FreeScout
2a804038ff Hide folders from Log Viewer 2026-06-06 20:35:14 -07:00
FreeScout
2cd2e66b77 Fix path traversal in Log Viewer - GHSA-9ph7-f3hc-95gg 2026-06-06 20:33:08 -07:00
FreeScout
47da5838f7 Upgrade symfony/polyfill-intl-idn to 1.38.1 - CVE-2026-46644 2026-06-06 00:06:17 -07:00
FreeScout
e2b4151e78 Patch symfony/routing - CVE-2026-45065 2026-06-06 00:03:14 -07:00
FreeScout
cd9e8fea9c Fix color of texts in logs table - closes #5442 2026-06-05 22:53:41 -07:00
FreeScout
e903accecd Fix formatting in tools.php 2026-06-05 22:52:36 -07:00
FreeScout
8bb0594e95 Improve tools.php security - GHSA-w2p9-3666-vw9j 2026-06-05 22:24:46 -07:00
FreeScout
11a542e3be Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2026-06-05 22:13:29 -07:00
FreeScout
8f7400492a v1.8.224 2026-06-05 22:12:49 -07:00
FreeScout
135d2d6d0c Enable browser check 2026-06-03 21:44:58 -07:00
FreeScout
5ec9169044 Improve sanitizing customer Websites field 2026-06-03 21:28:42 -07:00
FreeScout
5e664f2e25 Restrict .pht files upload - GHSA-27vp-fpg8-j8wv 2026-06-02 22:41:00 -07:00
FreeScout
c7e21d9abe Improve downloading log files logic - GHSA-858x-8f77-9vc5 2026-06-02 10:12:52 -07:00
FreeScout
0aab1d715f Fix user-setup empty invite_hash issue - GHSA-jqj5-r72v-v29g 2026-06-01 22:40:08 -07:00
FreeScout
d3c3148945 Add throttling to file upload routes - GHSA-ph4f-2jhx-q76w 2026-05-30 22:34:26 -07:00
FreeScout
7cb9f8c713 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2026-05-30 04:33:40 -07:00
FreeScout
fed0ecb533 v1.8.223 2026-05-30 04:33:27 -07:00
FreeScout
b7b4ce4069 Fix fetching message sent to multiple mailboxes from own mailbox - closes #5434 2026-05-29 04:54:25 -07:00
FreeScout
beef6af9b5 Fix prototype pollution in getQueryParam - GHSA-w5fc-8pp3-f755 2026-05-29 04:52:17 -07:00
FreeScout
afa0cb6ba5 Fix preg_replace_callback() error in Html2Text - closes #5433 2026-05-28 04:23:43 -07:00
FreeScout
26ff8c73ab Fix signature when moving conversation between mailboxes - closes #5419 2026-05-28 00:11:10 -07:00
FreeScout
30c0f68c97 Improve open tracking hash - closes #5431 2026-05-27 10:42:58 -07:00
FreeScout
875ab40739 Disable backward compatibility for old Message-IDs format on fetching - GHSA-8vm3-wwq4-ggfx 2026-05-25 21:22:05 -07:00
FreeScout
72525cce6c Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2026-05-25 01:39:16 -07:00
FreeScout
e39fa1299f v1.8.222 2026-05-25 01:39:04 -07:00
FreeScout
e2dcf6dc7e Send Auto Reply from the alias the customer emailed - closes #5428 2026-05-24 00:37:41 -07:00
FreeScout
28621dbfcf Update German translation 2026-05-23 07:35:26 -07:00
FreeScout
6ba03d32a0 Fix undefined signature_mailbox error on sending - closes #5427 2026-05-23 03:04:39 -07:00
FreeScout
25ea3d9388 Add vendor files to dist 2026-05-22 22:36:20 -07:00
FreeScout
a43239d8f0 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2026-05-22 22:35:57 -07:00
FreeScout
2fda21b54e v1.8.221 2026-05-22 22:35:46 -07:00
FreeScout
fc8f50c973 Show detailed error when uploading attachments - closes #5426 2026-05-21 22:56:07 -07:00
FreeScout
15db137829 Optimize Helper::stripDangerousTags() to avoid pcre.backtrack_limit hit - closes #5424 2026-05-21 11:21:10 -07:00
FreeScout
4a678b8309 Update zh-TW translation - closes #5421 2026-05-20 21:25:52 -07:00
FreeScout
1261b8b84c Fix signature when moving conversation between mailboxes - closes #5419 2026-05-20 03:34:54 -07:00
FreeScout
ae68e194b1 Improve permissions check when editing messages - GHSA-3w38-h42v-3h6w 2026-05-18 06:36:15 -07:00
FreeScout
5dab098380 Improve permissions check when deleting notes - GHSA-9vx8-gx3p-9mh6 2026-05-17 03:55:38 -07:00
FreeScout
215241ee2e Deprecate links to attachments without token - GHSA-wg74-ww4w-2qpc 2026-05-17 02:22:11 -07:00
FreeScout
f5c566c2e8 Update module activation logic 2026-05-17 01:56:43 -07:00
FreeScout
f3190f03c7 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2026-05-16 21:48:33 -07:00
FreeScout
3e216820ba v1.8.220 2026-05-16 21:48:23 -07:00
FreeScout
4008a22ef5 Fix checking trusted hosts during installation 2026-05-11 05:08:29 -07:00
FreeScout
fcea5716e5 Activate module after activating license 2026-05-10 04:23:00 -07:00
FreeScout
b455972062 Clear JS and CSS builds when clearing cache 2026-05-09 22:42:32 -07:00
FreeScout
a958553b72 Add configurable threshold to suppress transient fetch errors in Logs Monitoring - closes #5399 2026-05-09 05:36:27 -07:00
FreeScout
d902f19038 Check hash in replies to user email notifications - GHSA-6r38-6mcf-2ww3 2026-05-08 12:00:54 -07:00
FreeScout
49a9a4de64 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2026-05-08 10:32:36 -07:00
FreeScout
8e5e73b4ce v1.8.219 2026-05-08 10:29:34 -07:00
FreeScout
48f05f34e6 Fix PHP check in tools.php 2026-05-06 21:25:24 -07:00
FreeScout
ab8d681219 Fix error tracking on creating user profile from invite link - closes #5390 2026-05-06 01:34:00 -07:00
FreeScout
68746b8c17 Add Russian lang files 2026-05-02 23:09:50 -07:00
FreeScout
369eb9217a Add throttle to the Forgot Password form and return identical response regardless of whether the email exists - GHSA-jvmv-2qcp-7855 2026-05-02 23:06:28 -07:00
FreeScout
5a5a48e32c Add hash to open tracking URL - GHSA-qjr9-6v9q-3r72 2026-05-02 11:23:11 -07:00
FreeScout
79d5955ab1 Update SECURITY.md 2026-05-02 10:30:17 -07:00
FreeScout
d7fb065a5f Update German translation 2026-05-01 21:43:27 -07:00
FreeScout
956f631661 Add table prefix to raw DB queries - closes #5385 2026-05-01 03:51:05 -07:00
FreeScout
d5b63611ad Show message in the interface when browser does not support CSP 2026-04-28 07:48:55 -07:00
FreeScout
296a4c82a1 Fix error on PHP 7.1 - closes #5377 2026-04-27 03:50:34 -07:00
FreeScout
285ca977fa Add Catalan tranlation - closes #5376 2026-04-25 21:52:31 -07:00
FreeScout
a0b3888b18 Update German translation 2026-04-25 07:28:28 -07:00
FreeScout
518058aacf Add vendor files to dist 2026-04-24 21:55:55 -07:00
FreeScout
9fa67358ae Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2026-04-24 21:55:23 -07:00
FreeScout
f58b6d8bda v1.8.218 2026-04-24 21:55:09 -07:00
FreeScout
a7a2fe91e6 Add comment to APP_PROXY in .env.example 2026-04-24 02:42:52 -07:00
FreeScout
b0bf40295b Fix fetching emails into multiple mailboxes - closes #5368 2026-04-23 22:59:32 -07:00
FreeScout
da9bbfde8f Add APP_REMOTE_HOST_WHITE_LIST to .env.example 2026-04-23 21:24:09 -07:00
FreeScout
65219613b9 Deprecate use_new_pop3_lib config parameter 2026-04-23 05:34:22 -07:00
FreeScout
45356bfbdf Fix linking messages into conversations - closes #5372 2026-04-22 22:59:27 -07:00
FreeScout
c837ad77e5 Do not allow to merge convesation with self 2026-04-22 21:58:27 -07:00
FreeScout
164349549d Allow to use cidr in APP_REMOTE_HOST_WHITE_LIST - closes #5363 2026-04-22 21:23:17 -07:00
FreeScout
0c75dfde32 Allow to use cidr in APP_REMOTE_HOST_WHITE_LIST - closes #5363 2026-04-22 11:05:17 -07:00
FreeScout
87dba690ef Add upload_mode parameter to Helper::sanitizeUploadedFileName() 2026-04-22 11:00:27 -07:00
FreeScout
61adcbd890 Fix Helper::linkify() for links - closes #5367 2026-04-21 06:52:22 -07:00
FreeScout
190cdea748 Adjust waiting since block in Dashboard 2026-04-21 06:47:37 -07:00
FreeScout
53ce4523a4 Fix Helper::linkify() for emails - closes #5362 2026-04-20 10:58:38 -07:00
FreeScout
291ff9e306 Add indexes to tables - closes #5328 2026-04-20 10:26:36 -07:00
FreeScout
855b573672 Update SECURITY.md 2026-04-20 07:38:38 -07:00
FreeScout
3dcdbb85d6 Adjust tools.php 2026-04-20 06:05:38 -07:00
FreeScout
d8ec060a9d Require DB Password and check PHP Path direcotory in tools.php - GHSA-jx2w-fhmw-rg39 2026-04-20 06:03:32 -07:00
FreeScout
4fc258f77b Do not log Untrusted host error - closes #5361 2026-04-20 03:16:34 -07:00
FreeScout
25edcebd77 Patch PHPUnit - GHSA-qrr6-mg7r-m243 2026-04-19 23:58:03 -07:00
FreeScout
deb6788773 Fix decoding ISO-2022-JP emails - closes #5356 2026-04-19 21:55:46 -07:00
FreeScout
c4e4220ae3 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2026-04-19 07:20:04 -07:00
FreeScout
b729fcb2e1 v1.8.217 2026-04-19 07:19:54 -07:00
FreeScout
1371340962 Improve encryption and decryption with password via Helper 2026-04-18 21:48:32 -07:00
FreeScout
0dbf2e1665 Make user invite link expirable - GHSA-hqff-cwx7-3jpm 2026-04-18 05:18:06 -07:00
FreeScout
2293911718 Fix permissions check for user Notifications settings - GHSA-f489-qxv6-gvgg 2026-04-18 03:44:42 -07:00
FreeScout
864ad08820 Improve sanitizing Auto Reply message - GHSA-q3fh-rj9h-jfrc 2026-04-18 03:04:25 -07:00
FreeScout
270bd58f61 Fix redirect check in Helper::sanitizeRemoteUrl() - GHSA-22wf-848c-c856 2026-04-18 02:52:08 -07:00
FreeScout
60c01c3707 Fix Zipper: Path traversal detected error - closes #5354 2026-04-16 11:00:29 -07:00
FreeScout
17d31b62ad Fix On-Off switch on RTL - closes #5352 2026-04-16 03:32:52 -07:00
FreeScout
e6fe63e71f Add vendor files to dist 2026-04-16 03:19:11 -07:00
FreeScout
34677264d5 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2026-04-16 03:18:42 -07:00
FreeScout
66e5ac8e9d v1.8.216 2026-04-16 03:18:30 -07:00
FreeScout
43f6e154a5 Fix error in MailHelper::isGeneratedMessageId() - closes #5351 2026-04-15 23:22:46 -07:00
FreeScout
d61b74c306 Fix fetching into multiple mailboxes - closes #5350 2026-04-15 21:14:32 -07:00
FreeScout
95882ea8f1 PHP 8.5 compatibility 2026-04-15 07:45:18 -07:00
FreeScout
a7d678ac21 Fix mute icon for non admin users in Dashboard - closes #5348 2026-04-15 07:42:12 -07:00
FreeScout
4879a34b3d Fix search.title hook - closes #5347 2026-04-15 05:18:35 -07:00
FreeScout
01fa0955d4 Add URL to the error log message in CheckBrowser middleware 2026-04-15 03:22:05 -07:00
FreeScout
5f34e80a22 Fix OAuth disconnect link on PHP 7.1 - closes #5345 2026-04-15 00:57:59 -07:00
FreeScout
94485d86bb Fix moving conversations - closes #5343 2026-04-14 23:20:16 -07:00
FreeScout
ce20dfa787 Fix null parameter error in CheckBrowser - closes #5342 2026-04-14 10:56:07 -07:00
FreeScout
9a691124c6 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2026-04-14 07:44:53 -07:00
FreeScout
c703f38f6c v1.8.215 2026-04-14 07:44:45 -07:00
FreeScout
8a887502e1 Fix saving Sending and Fetching passwords - closes #5339 2026-04-14 07:43:41 -07:00
FreeScout
3368437e4e Fix 403 error in open tracking pixel - closes #5334 2026-04-14 07:29:56 -07:00
FreeScout
59cababa70 Fix error on creating user - closes #5337 2026-04-14 05:07:52 -07:00
FreeScout
b4d2846a3a Change Helper::stripTags() back to strip_tags() in some places 2026-04-14 03:59:19 -07:00
FreeScout
e45213e537 Add embed and appliet to the list of dangerous tags in Helper::stripDangerousTags() 2026-04-14 03:48:19 -07:00
FreeScout
c35fc2a502 Make conversation Unassigned when moving if assignee does not have access to the target mailbox - closes #5333 2026-04-13 21:58:24 -07:00
FreeScout
6583d6f5a5 For Assigned-only users show only assigned conversations in Search 2026-04-13 21:44:30 -07:00
FreeScout
25f92844c2 Create Open Middleware group 2026-04-13 21:26:35 -07:00
FreeScout
f4590b8279 Fix compact() Undefined variable operator - closes #5308 2026-04-13 21:18:48 -07:00
FreeScout
cdadaf621b Check permission when Assigned-only user is editing customer message 2026-04-13 11:37:54 -07:00
FreeScout
414878eb79 Check permission for Assigned-only users when editing drafts 2026-04-13 11:21:27 -07:00
FreeScout
aaf334e9bb Rename permission: Users are allowed to edit own notes/replies 2026-04-13 11:01:17 -07:00
FreeScout
f35fddb0cc Create Open Middleware group 2026-04-13 10:09:43 -07:00
FreeScout
fb130de64e Check permissions when settings chat_start_new for mailbox 2026-04-12 23:14:15 -07:00
FreeScout
5f182818e2 Sanitize attachments_to_remove when deleting attachments 2026-04-12 22:54:41 -07:00
FreeScout
eb397efae2 Add csrf_token to OAuth Disconnect link 2026-04-12 21:33:11 -07:00
FreeScout
14f17a5cd2 Check file paths in Zipper before extracting 2026-04-12 11:04:30 -07:00
FreeScout
abf82ecc3e Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2026-04-12 08:57:41 -07:00
FreeScout
d0713d5a67 v1.8.214 2026-04-12 08:53:24 -07:00
FreeScout
b33d1e56a8 Fix Browser check - closes #5331 2026-04-12 08:51:09 -07:00
FreeScout
c779afdda8 Check thread created_by_user_id when Undoing sending 2026-04-11 22:58:25 -07:00
FreeScout
83eea1ca47 Check customer visibility when creating Phone conversation 2026-04-11 03:40:30 -07:00
FreeScout
d459a3b0cf Check customer visibility when creating a customer within Change Customer dialog 2026-04-10 10:29:48 -07:00
FreeScout
b3d7611e6e Check if customer is accessible when changing conversation customer 2026-04-10 10:15:27 -07:00
FreeScout
2e2fe37111 Do not allow to move emails from customers from inaccessible mailboxes 2026-04-10 09:54:03 -07:00
FreeScout
c2680bd9ec Improve joining customer messages into conversations on Fetching - closes #5308 2026-04-10 08:45:38 -07:00
FreeScout
acf57a7a4b Add vendor files to dist 2026-04-09 10:30:34 -07:00
FreeScout
0425b26bc9 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2026-04-09 10:29:55 -07:00
FreeScout
88baece2a4 v1.8.213 2026-04-09 10:29:44 -07:00
FreeScout
265379b3ae Escape values in Helper::linkify() 2026-04-09 04:20:21 -07:00
FreeScout
39182e106a Send emails to all recipients when forwarding to multiple recipients - closes #5322 2026-04-09 02:36:09 -07:00
FreeScout
6f6419aafd Improve joining customer messages into conversations on Fetching - closes #5308 2026-04-08 23:00:05 -07:00
FreeScout
efe82e31b4 Validate host in mail Fetching and Sending settings 2026-04-08 07:41:53 -07:00
FreeScout
8babf94028 PHP 8.4 compatibility 2026-04-08 05:07:29 -07:00
FreeScout
1d83e1cffb Add CheckBrowser middleware ensuring that browser supports CSP 2026-04-08 03:24:41 -07:00
FreeScout
b2a1be6b3c Require mod_headers if Apache is used 2026-04-08 01:11:58 -07:00
FreeScout
f45b9105d4 Limit fields which can be populated on mailbox Fetching and Sending settings pages 2026-04-08 00:43:57 -07:00
FreeScout
97511c182b Escape customer name in the reply_fancy email template 2026-04-08 00:11:19 -07:00
FreeScout
b1d6c2c601 Improve Helper::getWebCronHash() 2026-04-07 23:04:36 -07:00
FreeScout
af3f1e2889 Improve joining customer messages into conversations on Fetching - closes #5308 2026-04-07 21:39:13 -07:00
FreeScout
a894ae1312 Apply safe_raw_html() function 2026-04-07 06:32:50 -07:00
FreeScout
ef5fbe30f4 Helper::stripInvalidUnicodeChars() function 2026-04-06 06:38:49 -07:00
FreeScout
1eb0469bd0 Check attachment name when downloading attachments 2026-04-06 06:16:27 -07:00
FreeScout
269e5ad424 Replace strip_tags() with Helper:stripTags() 2026-04-06 06:10:33 -07:00
FreeScout
5aa8d63321 Strip also style tags in Helper::stripDangerousTags() 2026-04-05 21:45:03 -07:00
FreeScout
e009c4675b Escape user name in flash message when deleting user 2026-04-05 21:19:35 -07:00
FreeScout
9131b16f80 Strip tags from name when creating user 2026-04-05 21:02:52 -07:00
FreeScout
c921033e8c Add mailbox.sidebar.buttons hook - closes #5316 2026-04-04 20:24:27 -07:00
FreeScout
dc886e7bb8 Fix Call to a member function close() on string in Zipper 2026-04-04 09:50:41 -07:00
FreeScout
7c73d5f168 Add allow_redirects to requests in WpApi 2026-04-04 09:43:34 -07:00
FreeScout
f35b4249c7 Take into account limit_user_customer_visibility parameter in load_customer_infoload_customer_info Ajax action 2026-04-04 04:49:33 -07:00
FreeScout
dbdf8f2260 Change attachment token generation algorythm 2026-04-03 22:13:36 -07:00
FreeScout
561eba496a Add vendor files to dist 2026-04-03 21:56:38 -07:00
FreeScout
00de71168b Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2026-04-03 21:53:38 -07:00
FreeScout
1de4e9b37a v1.8.212 2026-04-03 21:53:20 -07:00
FreeScout
726ef147eb Check if conversation IDs match when marking thread as read 2026-04-03 20:22:51 -07:00
FreeScout
b395a11791 Take limit_user_customer_visibility parameter into account when merging customers 2026-04-02 07:47:22 -07:00
FreeScout
2353c0bd1a Take limit_user_customer_visibility parameter into account when merging customers 2026-04-01 21:13:28 -07:00
FreeScout
b341dd86f7 Fix ncomplete object error on Status page - closes #5307 2026-04-01 07:32:09 -07:00
FreeScout
7e29e2463f Fix Passing null to parameter error in Html2Text - closes #5306 2026-03-31 20:50:16 -07:00
FreeScout
a8f3e558d7 Fix fetching emails with deeply nested HTML - closes #5304 2026-03-30 08:30:34 -07:00
FreeScout
55ff20a5d5 Update API URL 2026-03-28 23:45:30 -07:00
FreeScout
d8708d55e9 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2026-03-27 22:51:06 -07:00
FreeScout
afac233d5a v1.8.211 2026-03-27 22:50:43 -07:00
FreeScout
999cb49b58 Search conversation by correct number field - closes #5298 2026-03-26 23:54:23 -07:00
FreeScout
2810916a88 Add TrustHosts middleware 2026-03-26 10:32:36 -07:00
FreeScout
023882f9d9 Fix fetching emails containing null bytes in body - closes #5292 2026-03-26 06:30:32 -07:00
FreeScout
889d75c8e3 Add TrustHosts middleware 2026-03-25 23:15:34 -07:00
FreeScout
de69907c9d Add Helper::isSafePassword() function 2026-03-24 23:40:29 -07:00
FreeScout
b9ee220984 Allow to search conversations by number - closes #5251 2026-03-24 10:17:10 -07:00
FreeScout
4cea7689a4 Remove IMAP extension from list of required PHP extensions 2026-03-24 07:45:42 -07:00
FreeScout
ca6d5bb572 Fix Helper::checkIpByMask() function 2026-03-24 07:40:06 -07:00
FreeScout
33e8c60d89 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2026-03-22 21:55:28 -07:00
FreeScout
e489c2cc1d v1.8.210 2026-03-22 21:55:18 -07:00
FreeScout
a09a0f901d Add zh-TW language - closes #5285 2026-03-19 05:00:46 -07:00
FreeScout
729ee307e9 Fix playing audio attachments - closes #5281 2026-03-17 10:14:40 -07:00
FreeScout
486a2b9c15 Fix saving empty encrypted ENV values to via settings 2026-03-16 08:53:21 -07:00
FreeScout
b0b4e46139 Fix previewing attachments - closes #5275 2026-03-14 23:48:36 -07:00
FreeScout
0565ba97b7 Fix accessibility issues with magic checkboxes used in the conversations table - closes #4968 2026-03-14 21:53:26 -07:00
FreeScout
9ee5066922 Fix Undefined constant PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT - closes #5273 2026-03-14 21:28:43 -07:00
FreeScout
9272158c98 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2026-03-13 21:59:20 -07:00
FreeScout
ebe1913e12 v1.8.209 2026-03-13 21:59:10 -07:00
FreeScout
d5c34fb3c7 fix(webklex): Add Header.has(), called by Part.isAttachment() #5267 from boite/fix-missing-has
fix(webklex): Add Header.has(), called by Part.isAttachment()
2026-03-13 05:18:49 +01:00
boite
2b2d7b1c29 fix(webklex): Add Header.has(), called by Part.isAttachment()
This method does not exist and would result in a MethodNotFoundException
thrown by __call().
2026-03-12 22:56:08 +00:00
FreeScout
a664ac58a7 Fix iconv_mime_decode(): Detected an illegal character in input string - closes #5265 2026-03-12 11:12:24 -07:00
FreeScout
3329379db3 Include sanitized thread bodies in replies and email notifications 2026-03-11 00:19:12 -07:00
FreeScout
c1d2a1cbe5 Add CSP header when showing attachments 2026-03-10 21:53:19 -07:00
FreeScout
69d0504572 Add non_viewable_mime_types config parameter 2026-03-10 02:47:35 -07:00
FreeScout
cb86188457 Improve sanitizing SVG images containing comments 2026-03-09 23:26:11 -07:00
FreeScout
8bb8ea4140 Encrypt modules license keys 2026-03-09 23:05:31 -07:00
FreeScout
8045aaae95 Encrypt modules license keys 2026-03-09 23:02:04 -07:00
FreeScout
f5c3fe5610 Encrypt modules license keys 2026-03-08 07:34:26 -07:00
FreeScout
2a13430265 Encrypt OAuth tokens for IMAP and SMTP 2026-03-08 06:41:01 -07:00
FreeScout
1e319c91e1 Improve Helper::sanitizeRemoteUrl() function 2026-03-07 01:34:14 -08:00
FreeScout
cb6ea3284f Hide IMAP password in fetch command debug output 2026-03-05 23:57:25 -08:00
FreeScout
44c4cd0da8 Offer TLS encryption in Fetching and Sending settings by default 2026-03-05 22:15:01 -08:00
FreeScout
996a7f9633 Check access to mailbox when user edits customer thread 2026-03-05 21:40:51 -08:00
FreeScout
16e78f5b3e Check customer visibility on Customer Conversations page 2026-03-05 21:36:26 -08:00
FreeScout
b28d007210 Update French translation #5260 from JuliaLblnd/master
Update French translation
2026-03-06 05:58:58 +01:00
FreeScout
024c122758 Add vendor files to dist 2026-03-05 20:42:08 -08:00
FreeScout
c9d6ba4f68 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2026-03-05 20:41:00 -08:00
FreeScout
f13f07b5fe v1.8.208 2026-03-05 20:39:37 -08:00
Julia
e00480be02 Update French translation 2026-03-05 12:03:04 +01:00
FreeScout
5c2b2c7a51 Check access to mailbox when emptying folder 2026-03-04 20:55:24 -08:00
FreeScout
f0104169dc Adjust Helper::shellExec() function - closes #5250 2026-03-04 20:46:09 -08:00
FreeScout
411d8e7f91 Update SECURITY.md 2026-03-04 20:18:03 -08:00
FreeScout
5e08455cea Reduced Cc and Bcc in languages where the text does not fit nicely #5253 from il2020/master
Reduced Cc and Bcc in languages where the text does not fit nicely
2026-03-04 13:44:36 +01:00
il2020
08a6a53bdc Reduced Cc and Bcc in languages where the text does not fit nicely
https://github.com/freescout-help-desk/freescout/issues/5247
2026-03-04 12:37:55 +00:00
FreeScout
c7798de0e2 Update SECURITY.md 2026-03-04 04:16:43 -08:00
FreeScout
bd498e0a24 Disable blank issue in the repo 2026-03-04 04:12:45 -08:00
FreeScout
7cbbd5d187 Show errors on Status page when shell_exec() funciton can not be executed - closes #5250 2026-03-03 22:01:10 -08:00
FreeScout
f691461318 Do not show Cc and Bcc fields by default - closes #5247 2026-03-03 06:44:31 -08:00
FreeScout
a0d302dd0a Set curl_ssl_verifypeer parameter to true by default 2026-03-02 23:38:10 -08:00
FreeScout
b215d882d1 Improve sanitizing uploaded PDF files 2026-03-02 09:58:46 -08:00
FreeScout
19b94fcbb1 Fix incomplete object error on Status page - closes #5246 2026-03-02 05:02:47 -08:00
FreeScout
e76b683d85 Disable loading local config files in psy/psysh 2026-03-01 23:19:20 -08:00
FreeScout
2919365ca0 Symfony Process: Fix escaping for MSYS on Windows 2026-03-01 22:57:02 -08:00
FreeScout
c8621813b8 Fix unsafe unserialize() in phpunit 2026-03-01 22:31:31 -08:00
FreeScout
4b67a88227 Fix PATH_INFO issue in symfony/http-foundation 2026-03-01 22:09:05 -08:00
FreeScout
594f5b93af Update enshrined/svg-sanitize to v0.22.0 2026-03-01 22:04:23 -08:00
FreeScout
be848b8f63 Composer update 2026-03-01 22:02:59 -08:00
FreeScout
c77b77e774 Message-ID and Apache SpamAssassin - closes #5245 2026-03-01 21:36:29 -08:00
FreeScout
5d1b5e7c87 Message-ID and Apache SpamAssassin - closes #5245 2026-03-01 09:28:47 -08:00
FreeScout
a894d8c4f2 Fix permissions check when Following/Unfollowing conversation 2026-02-27 20:49:39 -08:00
FreeScout
aaa876a6d5 Adjust phpcs 2026-02-27 04:04:37 -08:00
FreeScout
6258a2d585 Adjust phpcs 2026-02-27 03:58:50 -08:00
FreeScout
581d351ba6 Fix phpcs notices 2026-02-27 03:53:37 -08:00
FreeScout
2f9a824ece Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2026-02-27 00:38:01 -08:00
FreeScout
e8dfd0fe68 Adjust phpcs 2026-02-27 00:35:42 -08:00
FreeScout
152e60f672 Add vendor files to dist 2026-02-26 22:15:24 -08:00
FreeScout
8c0e161f8d Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2026-02-26 22:14:32 -08:00
FreeScout
6f3dd89f75 v1.8.207 2026-02-26 22:14:22 -08:00
FreeScout
fae6417c40 Update phpcs 2026-02-26 22:11:39 -08:00
FreeScout
f7bc16c56a Perform sanitizing of the file name first in Helper::sanitizeUploadedFileName() 2026-02-26 09:25:15 -08:00
FreeScout
7c9f9635c5 PHP 8.5 compatibility - closes #5240 2026-02-26 06:40:26 -08:00
FreeScout
ee9cda1f0c PHP 8.5 compatibility - closes #5238 2026-02-26 05:34:54 -08:00
FreeScout
5a235a96a5 Goole Workspace OAuth - closes #5241 2026-02-26 02:32:31 -08:00
FreeScout
451ef5cf27 Goole Workspace OAuth - closes #5241 2026-02-26 00:26:09 -08:00
FreeScout
017a958b0e Fix Undefined array key error on sending reply to Phone conversation - closes #5236 2026-02-24 06:38:46 -08:00
FreeScout
a53818c147 Fix parsing email part Content-Type ending with semicolon 2026-02-23 20:44:39 -08:00
FreeScout
026989d523 Fix parsing email part Content-Type ending with semicolon 2026-02-23 10:02:29 -08:00
FreeScout
1d78708570 Add PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT to DB config - closes #5230 2026-02-22 21:21:17 -08:00
FreeScout
893c878c37 Check customer visibility when merging customers 2026-02-22 07:58:13 -08:00
FreeScout
accd83b835 Check user access to mailbox in empty_folder ajax action 2026-02-22 07:45:07 -08:00
FreeScout
b20a586989 Update tests matrix 2026-02-22 01:36:21 -08:00
FreeScout
3cd905274a Update tests matrix 2026-02-22 01:34:40 -08:00
FreeScout
d5708283d5 Update ConversationChangeCustomerTest 2026-02-22 01:26:53 -08:00
FreeScout
b8232fad6a Skip ConversationChangeCustomerTest on PHP < 8.4 2026-02-22 01:22:48 -08:00
FreeScout
6aca3bdda2 Skip ConversationChangeCustomerTest on PHP < 8.3 2026-02-22 01:12:11 -08:00
FreeScout
619db43999 Fix notices in phpunit 2026-02-22 01:03:32 -08:00
FreeScout
2101f72f4e PHP 8.3 compatibility 2026-02-22 00:51:51 -08:00
FreeScout
417e25dd44 Fix checking user authorization when changing conversation customer - closes #5232 2026-02-22 00:34:18 -08:00
FreeScout
3ef7bf2af8 Feature testing 2026-02-22 00:32:27 -08:00
FreeScout
5ab9cace19 Fix array_filter() in database config - closes #5230 2026-02-21 03:04:38 -08:00
FreeScout
63b225e380 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2026-02-21 00:59:14 -08:00
FreeScout
5710da0ccb v1.8.206 2026-02-21 00:58:55 -08:00
FreeScout
b134ee8939 Set allowed_classes parameter for unserialize() functions 2026-02-20 10:45:00 -08:00
FreeScout
db902c7f04 Remove role from fillable User fields 2026-02-19 23:08:02 -08:00
FreeScout
9984071e6f Extend Helper::restricted_extensions list 2026-02-19 22:47:02 -08:00
FreeScout
00d45dc954 Improve Helper::sanitizeUploadedFileName() function 2026-02-19 22:41:43 -08:00
FreeScout
004a8231f6 Improve TokenAuth middleware 2026-02-19 22:29:54 -08:00
FreeScout
1bfe3d5444 PHP 8.5 compatibility - closes #5227 2026-02-19 05:36:32 -08:00
FreeScout
587ae175c1 Update Customer addChannel() method - closes #5224 2026-02-18 22:25:26 -08:00
FreeScout
11b98a109a Add vendor files to dist 2026-02-14 00:27:45 -08:00
FreeScout
446e75e969 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2026-02-14 00:24:05 -08:00
FreeScout
e19633ce1a v1.8.205 2026-02-14 00:23:54 -08:00
FreeScout
6e36e39187 Fix styles for folder having .alert class - closes #5216 2026-02-12 09:32:14 -08:00
FreeScout
3991b68a0e PHP 8.5 compatibility - closes #5215 2026-02-12 05:54:07 -08:00
FreeScout
b76361f27d PHP 8.4 compatibility - closes #5214 2026-02-12 05:49:01 -08:00
FreeScout
387c94333e Add csp.form_action hook - closes #5189 2026-02-11 20:40:07 -08:00
FreeScout
9760bf620e Set max letngth for conversation subject - closes #5201 2026-02-08 04:26:37 -08:00
FreeScout
931c93506a Do now show CC and BCC fields when editing chat message draft - closes #5196 2026-02-07 22:07:30 -08:00
FreeScout
b01e0f7968 Fix wrong customer name after recipient change - closes #5199 2026-02-07 21:50:42 -08:00
FreeScout
d0d7f01924 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2026-02-07 20:57:32 -08:00
FreeScout
2bd7e35b57 Remove unneeded symlink 2026-02-07 20:56:51 -08:00
FreeScout
6fa9cd20f1 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2026-02-07 20:51:45 -08:00
FreeScout
47c085df03 v1.8.204 2026-02-07 20:51:27 -08:00
FreeScout
84c96abdd4 Update German translation 2026-02-07 03:16:29 -08:00
FreeScout
3711f0d0a3 Use PHP library to fetch via POP3 2026-02-06 04:14:17 -08:00
FreeScout
625f3b623d Remove IMAP extension from required extensions list 2026-02-06 04:11:40 -08:00
FreeScout
743c3241b5 Update Slovak translation 2026-02-03 01:45:39 -08:00
FreeScout
4e8e852530 Add conversation.skip_send_reply_to_customer hook - closes #5187 2026-02-02 10:26:59 -08:00
FreeScout
3a5cb77690 Add base-uri rule to CSP tag 2026-02-02 10:02:08 -08:00
FreeScout
2338a3dddf Remove Australia/Queensland timezone - closes #5175 2026-01-26 05:46:12 -08:00
FreeScout
661f7093f7 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2026-01-24 22:09:46 -08:00
FreeScout
06f6b4f6ac v1.8.203 2026-01-24 22:09:21 -08:00
FreeScout
f79e56f7d1 Fix invalid byte sequence for encoding UTF8 on PostgreSQL- closes #5159 2026-01-22 20:41:23 -08:00
FreeScout
e560512a03 Revert "Collapse email history in Gmail - closes #5136"
This reverts commit 7770ae10d2.
2026-01-22 02:00:23 -08:00
FreeScout
4752adbf35 Show customer email in conversations list - closes #5066 2026-01-18 20:59:55 -08:00
FreeScout
3454d28bd8 Remove customer email from conversations list - closes #5066 2026-01-16 20:41:14 -08:00
FreeScout
34d5973579 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2026-01-15 20:42:46 -08:00
FreeScout
7152ee7902 v1.8.202 2026-01-15 20:42:23 -08:00
FreeScout
cd56e5c3e0 Show scroll when there are many social profiles - closes #4644 2026-01-12 20:42:15 -08:00
FreeScout
bcee603c02 Show scroll when there are many social profiles - closes #4644 2026-01-12 20:38:08 -08:00
FreeScout
f3fb6ccb91 Show customer email in conversations list - closes #5066 2026-01-11 20:52:23 -08:00
FreeScout
c4c4263772 command.after_app_update hook - closes #5147 2026-01-11 06:46:45 -08:00
FreeScout
2c781425c8 Rename Headers tab to Email Headers - closes #5146 2026-01-10 20:59:10 -08:00
FreeScout
971a6915cc Update folders counters when deleting conversations forever 2026-01-06 02:18:21 -08:00
FreeScout
8c99abc8f4 Fix http_response_header error in Guzzle - closes #5137 2026-01-05 09:48:36 -08:00
FreeScout
1a4abad9c4 Add vendor files to dist 2026-01-05 02:42:40 -08:00
FreeScout
ec40bc80a0 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2026-01-05 02:41:41 -08:00
FreeScout
e4e099d5ac v1.8.201 2026-01-05 02:40:56 -08:00
FreeScout
7770ae10d2 Collapse email history in Gmail - closes #5136 2026-01-05 02:37:12 -08:00
FreeScout
c626c77411 PHP 8.4 compatibility 2026-01-03 02:38:51 -08:00
FreeScout
19d306454e Add array_first_cond() function 2025-12-29 08:44:22 -08:00
FreeScout
be4cd23e17 PHP 8.4 compatibility - closes #5134 2025-12-29 05:22:09 -08:00
FreeScout
94ce31599a PHP 8.4 compatibility for Guzzle RequestException - closes #5133 2025-12-29 01:07:44 -08:00
FreeScout
5883cbe9a4 PHP 8.5 2025-12-20 10:22:28 -08:00
FreeScout
c46e9a0acf Send email to correct customer when customer is changed - closes #5121 2025-12-19 06:27:50 -08:00
FreeScout
64cebaed0d Add vendor files to dist 2025-12-19 03:33:43 -08:00
FreeScout
28b73fe679 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2025-12-19 03:33:01 -08:00
FreeScout
d1087be216 v1.8.200 2025-12-19 03:32:54 -08:00
FreeScout
6e9132526a PHP 8.5 2025-12-19 02:28:24 -08:00
FreeScout
7cf1dad4db Fix CacheBasedSessionHandler PHP >= 8.1 - closes #5120 2025-12-17 09:46:39 -08:00
FreeScout
43576f53ca PHP 8.5 compatibility - closes #5114 2025-12-17 09:23:05 -08:00
FreeScout
061eabfadd PHP 8.5 compatibility - closes #5114 2025-12-16 10:18:25 -08:00
FreeScout
16f97bc086 Delete attachments when mailbox is deleted - closes #5108 2025-12-13 04:52:18 -08:00
FreeScout
c7bc54cf15 Sanitize email length before saving - closes #5106 2025-12-12 21:52:13 -08:00
FreeScout
e6cf128c3d Set last_reply_at when email conversation with one Note thread created - closes #5105 2025-12-12 21:44:37 -08:00
FreeScout
d9186546f6 Sanitize email length before saving - closes #5106 2025-12-11 22:57:37 -08:00
FreeScout
d8826584bc Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2025-12-10 20:29:09 -08:00
FreeScout
c632289e17 v1.8.199 2025-12-10 20:28:56 -08:00
FreeScout
97006884b5 Fix oAuth sending and Fftching - closes #5101 2025-12-10 02:52:30 -08:00
FreeScout
391a84ee06 Add mailbox.deleted hook 2025-12-09 10:09:06 -08:00
FreeScout
22495f5847 Fix oAuth sending and Fftching - closes #5101 2025-12-09 09:55:30 -08:00
FreeScout
694a89b757 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2025-12-05 21:51:10 -08:00
FreeScout
b1d35954a6 v1.8.198 2025-12-05 21:50:58 -08:00
FreeScout
2ecdad8114 Mark IMAP extension as optional on System Status page - closes #5055 2025-12-05 21:49:46 -08:00
FreeScout
c92e38a031 Make mysql_pass more random in install.sh - closes #5098 2025-12-05 10:23:40 -08:00
FreeScout
fc127d7dd4 Add APP_REMOTE_HOST_WHITE_LIST .env parameter 2025-12-01 09:49:29 -08:00
FreeScout
6981620c15 Sort mailboxes on user Permissions page - closes #5087 2025-11-27 08:16:15 -08:00
FreeScout
4f1b0180c1 Allow to specify email address in Username field for oAuth connections - #5051 2025-11-27 00:42:06 -08:00
FreeScout
4e46be5470 Add states parameter to Conversation getThreads() function 2025-11-26 23:11:48 -08:00
FreeScout
69c58b4871 Properly close conversations on agent reply by email - closes #5078 2025-11-24 00:40:47 -08:00
FreeScout
d4e1686ea0 More detailed error message for SMTP errors 2025-11-22 10:33:11 -08:00
FreeScout
ab2aa141ce Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2025-11-21 09:51:26 -08:00
FreeScout
d42a4dee4a v1.8.197 2025-11-21 09:51:12 -08:00
FreeScout
b0c32f9166 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2025-11-21 09:37:29 -08:00
FreeScout
62bd684642 v1.8.196 2025-11-21 09:37:19 -08:00
FreeScout
ea8505e2e5 Process customer names with comma - closes #5074 2025-11-21 02:12:24 -08:00
FreeScout
2855caeceb Add TLS v1.3 to SMTP connection - closes #5070 2025-11-19 10:08:25 -08:00
FreeScout
cc2bec702b More detailed error message for SMTP errors 2025-11-19 09:57:27 -08:00
FreeScout
7131be95fb Process situation when SMTP server does not reply after receiving mail data 2025-11-18 22:02:18 -08:00
FreeScout
229b156f7f Change smtp_timeout config parameter to 25 2025-11-18 14:18:16 -08:00
FreeScout
49309ade2d Write message to log when can not remove mutex in schedule:run 2025-11-18 10:27:25 -08:00
FreeScout
3f813a0432 Improve MSO comments processing in incoming emails - closes #5068 2025-11-18 06:34:11 -08:00
FreeScout
19e765f4b4 Add Slovenian language 2025-11-16 02:54:08 -08:00
FreeScout
b1a7daa2d5 RTL support for customer and notification emails - closes #5056 2025-11-09 02:23:28 -08:00
FreeScout
26527fa927 RTL support for emails - closes #5056 2025-11-08 04:37:21 -08:00
FreeScout
7c6d62b068 Update Hebrew translation 2025-11-07 20:38:49 -08:00
FreeScout
3a70c8b491 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2025-10-31 21:36:58 -07:00
FreeScout
99e1f2a7ce v1.8.195 2025-10-31 21:36:39 -07:00
FreeScout
3ac611f4b9 APP_USE_NEW_POP3_LIB parameter - closes #5048 2025-10-31 04:38:59 -07:00
FreeScout
7d2b40c524 Update Italian translation 2025-10-31 04:24:27 -07:00
FreeScout
d0161321cf Check user alternate emails when receveing @fwd emails - closes #5047 2025-10-29 04:43:07 -07:00
FreeScout
163a7f4745 Show mailbox email instead of user email in conversation print view - closes #5043 2025-10-27 06:32:01 -07:00
FreeScout
ad06917b88 Update Hebrew translation 2025-10-23 06:03:34 -07:00
FreeScout
6c7db6c883 Add Hebrew language 2025-10-21 10:21:23 -07:00
FreeScout
dfc6b002e0 Improve prefilling new conversation body via GET parameter 2025-10-18 02:30:47 -07:00
FreeScout
0f47d93c8b Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2025-10-17 09:42:42 -07:00
FreeScout
3b721788f2 v1.8.194 2025-10-17 09:42:26 -07:00
FreeScout
80a93ede92 Remove csp_enabled parameter from config 2025-10-17 09:41:27 -07:00
FreeScout
037eec6bbf Update Romanian translation 2025-10-14 02:48:34 -07:00
FreeScout
057c1d3e60 Improve Helper::cspMetaTag() function 2025-10-14 02:02:31 -07:00
FreeScout
950859ccec Improve Helper::cspMetaTag() function 2025-10-12 00:26:27 -07:00
FreeScout
a9ac255bc2 Fix searching customers via ajax on PostgreSQL - closes #5032 2025-10-11 04:49:07 -07:00
FreeScout
facbe21990 Add __j() and __h() to translation manager config 2025-10-11 04:23:54 -07:00
FreeScout
0ab988600d Update Polish translation 2025-10-07 22:57:57 -07:00
FreeScout
4e106cb0a7 Do not change conversation status to Active when reply to Spam conversation arrives - closes #5005 2025-10-07 03:14:26 -07:00
FreeScout
44d7a28616 Filter for web_notification person #5027 from Lars-/master
Filter for web_notification person
2025-10-07 06:53:40 +02:00
FreeScout
0870d9e94b Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2025-10-06 21:45:40 -07:00
FreeScout
e1c596a729 v1.8.193 2025-10-06 21:45:20 -07:00
Lars Jansen
6ae2b0fdbe feat: filter for web_notification person 2025-10-06 14:13:51 +02:00
FreeScout
6890cc38c9 Fix sending emails when some mailbox uses OAUTH - closes #5019 2025-10-05 21:52:54 -07:00
FreeScout
b590251393 body parameter for new-ticket - closes #5012 2025-10-05 11:22:16 -07:00
FreeScout
22ed3a775b Change out_username field maxlength to 255 - closes #5015 2025-10-05 10:52:39 -07:00
FreeScout
45aa0faf1d REDIS_CLIENT env parameter - closes #5009 2025-10-05 07:39:13 -07:00
FreeScout
701acfead9 Add mailbox_id-state-status index to conversations table - closes #4994 2025-09-06 02:36:50 -07:00
FreeScout
b0d589c3a4 Add vendor files to dist 2025-08-30 22:55:04 -07:00
FreeScout
f8f9400e06 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2025-08-30 22:53:10 -07:00
FreeScout
3bf9bb9611 v1.8.192 2025-08-30 22:53:02 -07:00
FreeScout
bc9bc6dffe Fix wrong user viewing conversation - closes #3801 2025-08-28 01:25:22 -07:00
FreeScout
27cef8055d Do not allow to merge customer with himself - closes #4986 2025-08-26 02:51:32 -07:00
FreeScout
76467c1771 Update Czech translation 2025-08-22 23:40:21 -07:00
FreeScout
c50226f4b3 PHP 8.4 compatibility - closes #4979 2025-08-22 23:23:04 -07:00
FreeScout
f9e238bb2c PHP 8.4 Compatibility - closes #4979 2025-08-21 05:06:16 -07:00
FreeScout
1373b3b939 Add vendor files to dist 2025-08-17 21:42:14 -07:00
FreeScout
3ad3cf5ab9 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2025-08-17 21:41:19 -07:00
FreeScout
ab7073f7fd v1.8.191 2025-08-17 21:41:01 -07:00
FreeScout
68964b5f9c PHP 8.4 compatibility 2025-08-14 04:10:42 -07:00
FreeScout
e53aece5b7 Fix selecting conversations using SHIFT - closes #4962 2025-08-10 05:35:30 -07:00
FreeScout
1e8483a886 Remove SQLite from installer - closes #4959 2025-08-08 11:46:47 -07:00
FreeScout
45da3085db Add web_notification.header hook #4961 from Lars-/master
Add web_notification.header hook
2025-08-08 18:43:20 +02:00
Lars Jansen
77e81c9598 feat: more flexibility for notifications 2025-08-08 16:11:07 +02:00
FreeScout
324d8d861e Fix IF error in update.sh - closes #4952 2025-08-02 23:05:34 -07:00
FreeScout
dc4d2c86ba Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2025-08-01 03:20:49 -07:00
FreeScout
19622ffd60 v1.8.190 2025-08-01 03:20:33 -07:00
FreeScout
251187d463 Add Ukrainian language 2025-07-30 23:32:50 -07:00
FreeScout
762e50d68e Change Receiving Emails to Fetching Emails in the warning in Connection Settings - closes #4949 2025-07-30 23:12:48 -07:00
FreeScout
c0cb3fa898 Add follow_redirects parameter to Helper::getRemoteFileContents() function 2025-07-29 03:57:36 -07:00
FreeScout
5b6e261c6a Sanitize URL in Helper::getRemoteFileContents() function 2025-07-28 05:02:42 -07:00
FreeScout
dcee75c499 Sanitize URL in Helper::getRemoteFileContents() function 2025-07-28 04:53:12 -07:00
FreeScout
aba50ed13d Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2025-07-25 23:04:55 -07:00
FreeScout
baa8512d8e v1.8.189 2025-07-25 23:04:20 -07:00
FreeScout
1e92ac8d7b Fix error in MailHelper::getHeader() - closes #4940 2025-07-24 01:05:15 -07:00
FreeScout
ecb28aef60 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2025-07-21 05:05:04 -07:00
FreeScout
be7e311ad0 v1.8.188 2025-07-21 05:04:16 -07:00
FreeScout
d8bcb53367 Fix Helper::decrypt() function - closes #492 2025-07-21 04:59:48 -07:00
FreeScout
a02cc2878e Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2025-07-21 03:24:07 -07:00
FreeScout
1426336439 v1.8.187 2025-07-21 03:23:26 -07:00
FreeScout
d3263c2429 Fix Helper::decrypt() function - closes #4929 2025-07-21 03:19:38 -07:00
FreeScout
4594a539a2 Optimize MailHelper::getHeader() 2025-07-20 23:12:33 -07:00
FreeScout
cf18c3850e Update Romanian language 2025-07-20 00:25:31 -07:00
FreeScout
53f6100f2c Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2025-07-19 04:29:38 -07:00
FreeScout
26a98823f9 v1.8.186 2025-07-19 04:28:38 -07:00
FreeScout
5fcc7de0a9 Add Romanian language 2025-07-19 04:26:32 -07:00
FreeScout
8814df7cfa Conversation threading for Outlook - closes #4922 2025-07-17 22:52:33 -07:00
FreeScout
b06e8928d2 Add allowed_tags parameter to Helper::stripDangerousTags() 2025-07-17 21:37:52 -07:00
FreeScout
304c0d4bca Add reply separator - closes #4764 2025-07-17 12:18:23 -07:00
FreeScout
62768d2112 Added actions to system status and tools pages - closes #4920 2025-07-17 12:14:46 -07:00
FreeScout
dedf9efe54 Add conversation.before_threads action #4917 from Lars-/master
Add conversation.before_threads action
2025-07-14 13:27:36 +02:00
Lars Jansen
09aafd50b2 feat: added conversation.before_threads action 2025-07-14 13:03:17 +02:00
FreeScout
9669c57f1d Strip also OBJECT tag in Helper::stripDangerousTags() 2025-07-14 02:12:56 -07:00
FreeScout
e2de65f3f3 Do not unserialize data by default in Helper::decrypt() function 2025-07-13 23:31:34 -07:00
FreeScout
1794763bbb Do not unserialize data by default in Helper::decrypt() function 2025-07-10 08:37:12 -07:00
FreeScout
a4e9e89518 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2025-07-05 02:36:31 -07:00
FreeScout
4038bbe6b5 v1.8.185 2025-07-05 02:36:18 -07:00
FreeScout
09558dab75 In cCat mMde expand Show Details block if it contains invalid fields 2025-07-04 23:17:10 -07:00
FreeScout
0298d7f3c6 Add start new chat option to mailbox - closes #4902 2025-07-04 22:48:05 -07:00
FreeScout
a58360926a Allow to prefill multiple emails via GET parameter - closes #4896 2025-06-29 10:58:13 -07:00
FreeScout
b3f1868b42 Fix group keys error in Translation manager 2025-06-26 11:18:31 -07:00
FreeScout
86b30fe336 Add Arabic language 2025-06-25 03:50:54 -07:00
FreeScout
e4f471ef28 Update Russian translation 2025-06-25 03:27:36 -07:00
FreeScout
e85c3be9ff Show error on POP3 failed authentication 2025-06-24 23:13:00 -07:00
FreeScout
b3e64dcfb1 PHP 8.4 compatibility - closes #4881 2025-06-23 22:58:10 -07:00
FreeScout
c0ae0ca567 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2025-06-22 05:38:20 -07:00
FreeScout
5638e46fab v1.8.184 2025-06-22 05:38:07 -07:00
FreeScout
b5b79fd298 Update Czech translation 2025-06-21 23:20:29 -07:00
FreeScout
806e094cd5 Fix mb_convert_encoding() error in decodeBoundary() on fethcing 2025-06-21 23:01:58 -07:00
FreeScout
a454c0cd57 Fix too many redirects issue after assigning a conversation in some cases - closes #4009 2025-06-20 00:04:02 -07:00
FreeScout
98a94c3e6b Add missing schedule options for fetch command #4873 from sulemaanhamza/master
Add missing schedule options for fetch command
2025-06-19 14:59:24 +02:00
suleman
b1b24f3114 Add missing schedule options for fetch command 2025-06-19 15:47:50 +05:00
FreeScout
570e3b9932 Fixes in Laravel Translation Manager - CVE-2025-49130 2025-06-17 11:39:20 -07:00
FreeScout
fb2fd080fe Add vendor files to dist 2025-06-13 23:18:43 -07:00
FreeScout
6a27f13f3c Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2025-06-13 23:17:55 -07:00
FreeScout
382a938b14 v1.8.183 2025-06-13 23:17:38 -07:00
FreeScout
e1abd03937 PHP 8.4 compatibility - closes #4848 2025-06-10 10:23:21 -07:00
FreeScout
3e05686342 Fix Call to a member function getFullName() on null on fetching - closes #4845 2025-06-10 03:42:18 -07:00
FreeScout
9aa8299720 Remove blockquote gmail_quote from the list of reply separators 2025-06-05 11:12:41 -07:00
FreeScout
fbc0376443 Set RFC822.eml name for attached .MSG and .EML files - closes #4738 2025-06-03 05:33:50 -07:00
FreeScout
7a0f0d64e0 Do not send email notifications if mail server send limit reached - closes #4806 2025-06-02 23:27:55 -07:00
FreeScout
ca675b0815 Add vendor files to dist 2025-06-02 23:00:31 -07:00
FreeScout
a3e6826b2f Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2025-06-02 22:59:34 -07:00
FreeScout
395318b568 v1.8.182 2025-06-02 22:59:17 -07:00
FreeScout
d6483f895c Fix showing Delete button in bulk actions - closes #4819 2025-06-01 11:24:56 -07:00
FreeScout
b740d55260 Fix saving their profiles by support agents 2025-05-30 00:00:08 -07:00
FreeScout
da62f64f34 Add hook to ReplyToCustomer.php - closes #4815 2025-05-29 11:54:42 -07:00
FreeScout
0a9a51c73c Fix error on uploading user photo - closes #4812 2025-05-28 11:02:36 -07:00
FreeScout
fcc7437ffa Fix PHP 8.4 error in Swift/TransportException.php - closes #4809 2025-05-28 03:55:24 -07:00
FreeScout
18fc0bc54d Fix null parameter issue in imap_rfc822_parse_headers() - closes #4807 2025-05-27 10:39:45 -07:00
FreeScout
95fe52218e Add __h() function 2025-05-27 06:40:53 -07:00
FreeScout
fb346f2e2e Add vendor files to dist 2025-05-27 00:01:08 -07:00
FreeScout
b0804c6dca Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2025-05-26 23:59:31 -07:00
FreeScout
7442fbb522 v1.8.181 2025-05-26 23:58:44 -07:00
FreeScout
54b9548f8c Add __j() function 2025-05-26 23:56:10 -07:00
FreeScout
8810c48f97 Show translated messages when activating or deactivating modules 2025-05-25 10:02:48 -07:00
FreeScout
4fd28b3038 Escape quotes when publishing translations 2025-05-25 09:48:57 -07:00
FreeScout
c624e8e781 Add md size to modal dialogs 2025-05-25 09:02:40 -07:00
FreeScout
81bf63f8b0 Suppress Duplicate entry error when fetching emails - closes #4795 2025-05-23 06:20:11 -07:00
FreeScout
5dd7a2effb Do not trim values enclosed in quotes in sanitiseVariableValue() - closes #4714 2025-05-23 05:54:52 -07:00
FreeScout
8484785a17 Take into account APP_SHOW_ONLY_ASSIGNED_CONVERSATIONS option when deleting conversation 2025-05-23 05:36:57 -07:00
FreeScout
3f5bb2841f Eliminate possibility of race condition when deleting a user 2025-05-23 03:01:10 -07:00
FreeScout
508dda1685 Strip tags in User::setData() method 2025-05-23 02:38:50 -07:00
FreeScout
9dc18b4b52 Fix error on creating mailbox - closes #4800 2025-05-23 02:02:25 -07:00
FreeScout
9879e29c20 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2025-05-22 19:29:49 -07:00
FreeScout
59a2975cc0 v1.8.180 2025-05-22 19:29:37 -07:00
FreeScout
cd98d8b559 Add fetch_emails.separate_reply.preprocess_body filter - closes #4791 2025-05-22 19:27:31 -07:00
FreeScout
305f45a03d Sanitize name field when creating mailbox 2025-05-22 12:08:29 -07:00
FreeScout
7718d8e906 Strip dangerous tags in /system/ajax-html/job_details/ modal 2025-05-22 11:57:56 -07:00
FreeScout
540b3cba6e Strip dangerous tags when publishing translations 2025-05-22 11:23:00 -07:00
FreeScout
b5574ea37b Set same_site back to null in session config - closes #4769 2025-05-22 11:03:15 -07:00
FreeScout
ac13321407 Make setCookie() function crossbrowser in main.js - closes #4793 2025-05-22 10:59:55 -07:00
FreeScout
47c516ef9a Escape company name displayed in the Welcome to Company flash message 2025-05-21 11:39:56 -07:00
FreeScout
20034ab376 Escape customer name in the customer updated flash message 2025-05-21 11:31:17 -07:00
FreeScout
4fb8b7f8d7 Remove also unclosed restricted tags in Helper::stripDangerousTags() function 2025-05-21 03:21:09 -07:00
FreeScout
38316da137 Limit fields which can be updated during updating customer 2025-05-20 23:10:05 -07:00
FreeScout
507be36664 Limit fields which can be updated in OpenController userSetupSave() function 2025-05-20 11:16:30 -07:00
FreeScout
8e3c87ac55 Check language name when deleting language from Translate page 2025-05-20 02:55:45 -07:00
FreeScout
d2048f59a8 Limit fields which can be updated during user creation 2025-05-20 02:45:50 -07:00
FreeScout
119a2772ec Limit fields which can be updated on mailbox Settings and Auto Reply pages 2025-05-20 02:40:12 -07:00
FreeScout
1628567cfc Do not allow regular users change other users emails when PERM_EDIT_USERS enabled 2025-05-19 03:54:12 -07:00
FreeScout
f0b8e63897 Fix fix UserPolicy update method 2025-05-19 03:47:19 -07:00
FreeScout
6eb8bdad2e Do not allow to change user password in profileSave() function 2025-05-19 03:38:55 -07:00
FreeScout
d797aacb9a Fix CSP issue when embedding iframe - closes #4777 2025-05-19 03:01:15 -07:00
FreeScout
e804038b4e Fix German translation - closes #4776 2025-05-18 22:35:14 -07:00
FreeScout
1f154ce039 Take into account APP_LIMIT_USER_CUSTOMER_VISIBILITY option on customer profile page 2025-05-18 11:15:43 -07:00
FreeScout
eb0be440ac Take into account APP_SHOW_ONLY_ASSIGNED_CONVERSATIONS option on customer Conversation tab 2025-05-18 04:28:38 -07:00
FreeScout
cfdcdaf092 Take into account APP_SHOW_ONLY_ASSIGNED_CONVERSATIONS option when following or unfollowing conversation 2025-05-18 04:19:24 -07:00
FreeScout
43dafd9130 Take into account APP_SHOW_ONLY_ASSIGNED_CONVERSATIONS option when changing conversation subject 2025-05-18 04:17:27 -07:00
FreeScout
87cdb65d6b Take into account APP_SHOW_ONLY_ASSIGNED_CONVERSATIONS option in conversation update policy 2025-05-18 04:11:52 -07:00
FreeScout
0a8fe89a92 Add vendor files to dist 2025-05-18 03:44:23 -07:00
FreeScout
d9f96d4e03 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2025-05-18 03:43:34 -07:00
FreeScout
ce607ff111 v1.8.179 2025-05-18 03:43:15 -07:00
FreeScout
2552a2b842 Check from_thread_id parameter access permissions when creating new conversations 2025-05-17 11:17:14 -07:00
FreeScout
01c91d2086 Check access permissions when muting mailbox notifications via Ajax 2025-05-17 06:52:19 -07:00
FreeScout
e136660e8d Add phtml and phar to the list of restricted file upload extensions 2025-05-16 23:22:22 -07:00
FreeScout
f411f8cf5a Fix CVE-2024-52301 issue in laravel/framework 2025-05-16 11:39:58 -07:00
FreeScout
da7898446f Add extra reply separator - closes #4764 2025-05-16 06:08:45 -07:00
FreeScout
5620c25830 Fix array_merge error in Option::set() - closes #4759 2025-05-15 09:19:58 -07:00
FreeScout
fa3f6c8c63 Allow external styles and fonts in the CSP tag - closes #4755 2025-05-15 04:47:22 -07:00
FreeScout
7a2f6d1e12 Adjust CSP tag to display external images - closes #4754 2025-05-15 04:01:39 -07:00
FreeScout
eab9771102 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2025-05-14 23:34:52 -07:00
FreeScout
8267be397f v1.8.178 2025-05-14 23:34:35 -07:00
FreeScout
4e82464f48 Set SameSite to None for session cookie and others - closes #4750 2025-05-14 23:32:20 -07:00
FreeScout
f03810e380 Do not use serialize() function in Option::set() 2025-05-14 22:38:44 -07:00
FreeScout
fb33d672a2 Improve sanitizing php_path in tools.php 2025-05-14 11:44:36 -07:00
FreeScout
311319199e Do not use serialize() function in Option::set() 2025-05-14 11:00:29 -07:00
FreeScout
17421bae24 Fix VerifyCsrfToken::handle() function - closes #4752 2025-05-14 06:45:01 -07:00
FreeScout
f9f491c453 Fix TokenMismatchException not found - closes #4753 2025-05-14 06:38:18 -07:00
FreeScout
d7d7a020d3 Do not allow to create translations with random language code 2025-05-14 04:22:02 -07:00
FreeScout
f7548a7076 Do not user serialize() function in Option::set() 2025-05-14 04:05:17 -07:00
FreeScout
018bb1f982 Remove new line symbols in Helper::setEnvFileVar() 2025-05-14 02:43:05 -07:00
FreeScout
916fe1102f Adjust default-src in CSP tag - closes #4749 2025-05-14 02:22:06 -07:00
FreeScout
9070732d2c Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2025-05-13 12:03:41 -07:00
FreeScout
4f8d998068 v1.8.177 2025-05-13 12:03:20 -07:00
FreeScout
8b07e919ad Rollback CSP tag changes - closes #4749 2025-05-13 12:01:56 -07:00
FreeScout
e4b21c9797 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2025-05-13 11:07:45 -07:00
FreeScout
0fb7bb3aa5 v1.8.176 2025-05-13 11:07:26 -07:00
FreeScout
823816517e Add new reply separator - closes #4629 2025-05-12 22:49:19 -07:00
FreeScout
5f8a7feb2a Adjust reply separators - closes #4629 2025-05-12 06:36:38 -07:00
FreeScout
9102717053 Adjust reply separators - closes #4629 2025-05-12 04:15:55 -07:00
FreeScout
2cc4fb3c5c Set X-Content-Type-Options header and remove unwanted headers 2025-05-09 11:30:41 -07:00
FreeScout
9ab1dae25e Do not set XSRF-TOKEN cookie as it is not used 2025-05-09 11:17:42 -07:00
FreeScout
10cafafeef Set SameSite to Lax by default in setCookie function in main.js 2025-05-09 10:53:35 -07:00
FreeScout
0f1d3fa9fc Set SameSite attribute to lax for session cookie 2025-05-09 10:49:59 -07:00
FreeScout
694403e0ce Add form-action to CSP meta tag 2025-05-09 06:05:48 -07:00
FreeScout
8274a96d7c Add default-src to CSP meta tag 2025-05-09 06:01:06 -07:00
FreeScout
c980d52ff0 Do not stop fetching when encountering an email without Message-ID - closes #4740 2025-05-08 07:34:43 -07:00
FreeScout
0b6a314f01 Take in account possible spaces when handling forwarded emails - closes #4733 2025-05-04 06:47:47 -07:00
FreeScout
03992b2061 Add action to append customer profile tab #4725 from simivar/patch-2
Add action to append customer profile tab
2025-04-29 07:51:51 +02:00
Krystian Marcisz
44bf284fe3 feat: add action to append profile tab
Adds a new actions, `customers.profile_tabs.append`, for easy appending of new tabs in the Customer's profile
2025-04-28 22:32:44 +02:00
FreeScout
1ced45029e Add vendor files to dist 2025-04-25 23:57:06 -07:00
FreeScout
639c08c1f3 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2025-04-25 23:56:32 -07:00
FreeScout
9e0ef60e00 v1.8.175 2025-04-25 23:56:22 -07:00
FreeScout
fd4b334d8a Webklex - Fix Date parsing on non-standard format from Aqua Mail 2025-04-25 04:00:41 -07:00
FreeScout
a8a2bded0a Webklex - Fix case sensitivity of folder attribute parsing: NoSelect, NoInferiors 2025-04-25 03:50:28 -07:00
FreeScout
ca003cecb2 Webklex - Fix Get folders list in hierarchical order 2025-04-25 03:45:42 -07:00
FreeScout
7ad231787b Webklex - When using the chunk function, some messages do not have an element with index 0 2025-04-25 03:43:35 -07:00
FreeScout
17cfded1f0 Added extra user actions and filters - closes #4715 2025-04-23 03:50:35 -07:00
FreeScout
7a2f965951 Added extra user actions and filters #4715 from Lars-/feature/notifications
Added extra user actions and filters
2025-04-23 12:48:19 +02:00
FreeScout
22ef2b48d1 Update email.user_notification.subject Eventy filter #4713 from avenjamin/master
Update email.user_notification.subject Eventy filter
2025-04-23 12:40:34 +02:00
Lars Jansen
4ab35da9cf chore: changed action to use less parameters 2025-04-23 12:09:42 +02:00
Lars Jansen
4b6171c9cd chore: changed modifications to actions and filters 2025-04-23 12:05:42 +02:00
Ben Perry
174b7dcf36 Update email.user_notification.subject Eventy filter
Added `$this->conversation` parameter to allow the Ticket Module to override the email subject.
2025-04-23 15:09:01 +10:00
FreeScout
a086151b2f Prevent zooming editor input in iOS browser - #4698 2025-04-12 20:51:29 -07:00
FreeScout
de4d6ed2fc PHP 8.4 compatibility - closes #4695 2025-04-09 04:38:41 -07:00
FreeScout
f142f0e575 Add mail fallback capabilities to mail vars #4693 from caseproof/add/mail-vars-fallback
Add mail fallback capabilities to mail vars
2025-04-08 08:27:41 +02:00
Thomas Patrick Levy
affa9325df feat: add mail vars fallback 2025-04-07 17:06:53 -07:00
Thomas Patrick Levy
56e8aa2669 Merge branch 'master' of github.com:freescout-help-desk/freescout 2025-04-07 16:17:08 -07:00
FreeScout
1839a43359 Add vendor files to dist 2025-04-04 05:38:45 -07:00
FreeScout
64d21bcecf Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2025-04-04 05:37:46 -07:00
FreeScout
2db9233ba1 v1.8.174 2025-04-04 05:37:32 -07:00
FreeScout
d154d31ec2 Fix UnclosedFormControlIsInvalid error in the editor - closes #4671 2025-04-03 23:23:08 -07:00
FreeScout
9fe476b742 Remove RLM characters when sanitizing file names - closes #2123 2025-04-03 23:02:46 -07:00
FreeScout
f91396f833 Improve PopProtocol.php 2025-04-02 23:08:34 -07:00
FreeScout
beabd14cae Update Readme 2025-04-01 23:59:04 -07:00
FreeScout
a2d81deff9 Fix retrieving IMAP folders containing single quotes in names - closes #4682 2025-04-01 23:57:25 -07:00
FreeScout
0e83d360b5 Remove unprintable characters and invalid unicode characters from uploaded file names - closes #4681 2025-04-01 05:30:33 -07:00
FreeScout
f05fe74f4e Do not hide attachments after editing notes or messages - closes #4678 2025-03-31 05:49:45 -07:00
FreeScout
cc87962474 Fix error when job payload is empty on Status page - closes #4673 2025-03-31 04:51:37 -07:00
FreeScout
10c32901d8 Add Hungarian language 2025-03-31 01:23:15 -07:00
FreeScout
d1dffe8f60 Take into account user time zone and time format in Customer viewed text - closes #4674 2025-03-30 03:20:39 -07:00
FreeScout
04ec0b09fe Timezone fix for Kolkata #4676 from vtq221/master
Timezone fix for Kolkata
2025-03-30 09:38:14 +02:00
vtq
3651fcdcc2 Timzone fix for Kolkata 2025-03-29 10:01:50 +11:00
FreeScout
feda4104b0 PHP 8.4 compatibility - closes #4630 2025-03-09 04:56:17 -07:00
FreeScout
8bf8c929fb Change Helper::IN_LIMIT to 999 to avoid issues on MariaDB - closes #4623 2025-03-07 07:07:20 -08:00
FreeScout
7f824c685e Show Link modal dialog faster - closes #4622 2025-03-05 05:16:51 -08:00
FreeScout
7f2f83d32e Show modal dialogs faster - closes #4622 2025-03-04 23:54:03 -08:00
FreeScout
b13e0fefb0 Show empty link URL when inserting link - closes #4622 2025-03-04 23:45:19 -08:00
FreeScout
dbeba27553 Trigger thread.before_save_from_request hook for each thread when sending to multiple recipients 2025-03-02 05:37:56 -08:00
FreeScout
90cd9061b4 Fix Call to a member function query() on null when testing fetching - closes #4614 2025-02-28 06:19:39 -08:00
FreeScout
7991cb39a5 Fix Call to a member function query() on null when testing fetching - closes #4614 2025-02-28 06:15:39 -08:00
FreeScout
31c72b8092 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2025-02-28 00:15:15 -08:00
FreeScout
5e56a473e7 v1.8.173 2025-02-28 00:14:38 -08:00
FreeScout
497e481d42 Allow to specify module.json in module latestVersionNumberUrl parameter - closes #4611 2025-02-27 23:41:39 -08:00
FreeScout
ade91429e8 Allow to spcify path to GitHub zip archive in module latestVersionZipUrl parameter - closes #4611 2025-02-26 22:45:27 -08:00
FreeScout
b9effba813 Show customer ZIP in the customer sidebar - closes #4606 2025-02-26 06:05:35 -08:00
FreeScout
0b1d72d68e Use keyMap Enter insertParagraph in editor as it insert divs #4607 from Som-Energia/fix-editor-insert-paragraph
Use keyMap Enter->insertParagraph in editor as it insert divs
2025-02-26 14:54:34 +01:00
Àngel Fernández
6c5ad6997d Use keyMap Enter->InserParagraht in editor as it insert divs 2025-02-26 12:56:29 +01:00
FreeScout
4df0c3196f Fix loadHTML() error on fetching - closes #4600 2025-02-25 09:53:01 -08:00
FreeScout
b8f82a15a9 Add vendor files to dist 2025-02-21 00:49:59 -08:00
FreeScout
792cb95718 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2025-02-21 00:49:02 -08:00
FreeScout
cebd754983 v1.8.172 2025-02-21 00:48:51 -08:00
FreeScout
e69a4105a1 Handling boundary split into several parts in email headers - closes #4567 2025-02-20 23:32:24 -08:00
FreeScout
1ff8dce33c Use ajax search in the customer merge select 2025-02-18 21:26:00 -08:00
FreeScout
5b74800cb4 Use ajax search in the customer merge select 2025-02-18 08:49:42 -08:00
FreeScout
199bfc5dca PHP 8.4 compatibility - closes #4588 2025-02-18 01:09:04 -08:00
FreeScout
5ea7d041b2 Merge customers - closes #2188 2025-02-17 23:59:51 -08:00
FreeScout
ad183694bf Handling boundary split into several parts in email headers - closes #4567 2025-02-17 00:18:04 -08:00
FreeScout
d77fa741c8 PHP 8.4 compatibility 2025-02-14 21:14:13 -08:00
FreeScout
ec4cf9a457 Add vendor files to dist 2025-02-14 03:33:22 -08:00
FreeScout
f7e20304b2 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2025-02-14 03:32:28 -08:00
FreeScout
f51651e45a v1.8.171 2025-02-14 03:32:07 -08:00
FreeScout
d179d96bde Update Swedish translations #4578 from timhultstrand/master
Update Swedish translations
2025-02-14 06:25:13 +01:00
timhultstrand
c7498f288e Update Swedish translations 2025-02-13 21:51:19 +01:00
FreeScout
f691fe2d7f Show which attachments are embedded in freescout:parse-eml command - closes #4575 2025-02-13 03:22:14 -08:00
FreeScout
6f1c286af3 Fix Undefined variable $protonmail_quote_pos on fetching - closes #4568 2025-02-11 04:02:45 -08:00
FreeScout
b43e300b7c PHP 8.4 compatibility 2025-02-11 04:01:00 -08:00
FreeScout
a23f64d539 Allow to enable persistent connections for MySQL and PostgreSQL #4566 from RisingOpsNinja/db-persistent-connections
Allow to enable persistent connections for MySQL and PostgreSQL
2025-02-10 15:07:08 +01:00
RisingOpsNinja
dcfa6d4f9c Allow to enable persistent connections for MySQL and Postgresql
Request a persistent db connection, rather than creating a new connection for every request.
2025-02-10 12:37:08 +01:00
FreeScout
791bc1d643 PHP 8.4 compatibility 2025-02-08 23:10:37 -08:00
FreeScout
bb178de5d2 Fix searching conversations and customers on PostgreSQL - closes #4559 2025-02-08 07:48:15 -08:00
FreeScout
33e000af6f PHP 8.4 compatibility 2025-02-08 06:00:08 -08:00
FreeScout
8c1c4c1a03 PHP 8.4 compatibility 2025-02-07 09:19:23 -08:00
FreeScout
35b628376b Allow to set PDO::PGSQL_ATTR_DISABLE_PREPARES via environment variable #4558 from RisingOpsNinja/master
Allow to set PDO::PGSQL_ATTR_DISABLE_PREPARES via environment variable
2025-02-07 16:52:33 +01:00
RisingOpsNinja
79bc595e14 Allow to set PDO PGSQL_ATTR_DISABLE_PREPARES via environment variable 2025-02-07 16:28:28 +01:00
FreeScout
d9ada9c6ec Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2025-02-07 05:22:47 -08:00
FreeScout
ee361fd95a v1.8.170 2025-02-07 05:22:38 -08:00
FreeScout
3d517b4434 Fix empty body issue caused by incrorrect proton reply fix - closes #4556 2025-02-07 05:21:52 -08:00
FreeScout
d95ab52c4d Add vendor files to dist 2025-02-06 23:40:39 -08:00
FreeScout
4d889e31e4 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2025-02-06 23:39:40 -08:00
FreeScout
dabc022c4f v1.8.169 2025-02-06 23:39:10 -08:00
FreeScout
3c709fa3ba Fix deleting attachments when creating a new conversation from existing thread - closes #4432 2025-02-06 22:10:06 -08:00
FreeScout
95c2a1a2f4 Disable routes caching as it does not increase speed and increases memory consumption - closes #4544 2025-02-04 09:32:17 -08:00
FreeScout
bab2fc5fb8 Enable routes caching - closes #4544 2025-02-04 01:55:55 -08:00
FreeScout
cf94762736 Update Gmail reply separator 2025-02-02 22:23:58 -08:00
FreeScout
94269e1dea Fix separating replies from Proton emails - closes #4537 2025-02-02 22:20:46 -08:00
FreeScout
34b910bbba Fix getFullName() on null in Conversation.php - closes #4540 2025-01-31 08:42:39 -08:00
FreeScout
fbcd1797f0 Remove services.php and packages.php in /bootstrap/cache/ when clearing cache - closes #4536 2025-01-31 03:20:21 -08:00
FreeScout
cb0e560ed8 Remove services.php and packages.php in /bootstrap/cache/ when clearing cache - closes #4536 2025-01-31 03:11:06 -08:00
FreeScout
589ac69594 Increase mailbox imap_sent_folder field max length to 50 - closes #4533 2025-01-30 03:42:23 -08:00
FreeScout
e4dfd3cb2e Revert editor blockquote fix - closes #3811 2025-01-29 06:16:20 -08:00
FreeScout
490475fe56 Add searching by user fullname for conversations and customers #4523 from caseproof/add/user-fullname-search
Add searching by user fullname for conversations and customers
2025-01-29 06:35:01 +01:00
Thomas Patrick Levy
65014d72b5 feat: add searching by user fullname for conversations and customers 2025-01-28 11:34:08 -07:00
FreeScout
2dd4260103 PHP 8.4 compatibility 2025-01-28 04:45:01 -08:00
FreeScout
7c00262494 Make encoding check case-insensitive in Header.php - closes #4519 2025-01-28 04:31:56 -08:00
FreeScout
4ab9e0beb8 PHP 8.4 compatibility 2025-01-27 22:10:12 -08:00
FreeScout
6608107d70 Change PHP packages installation order to avoid installing apache - closes #4514 2025-01-25 23:02:51 -08:00
FreeScout
cf08353ebc Update German translation 2025-01-24 22:25:40 -08:00
FreeScout
5c73a68d12 PHP 8.4 compatibility 2025-01-24 22:01:38 -08:00
FreeScout
ef70f04594 PHP 8.4 compatibility 2025-01-24 21:53:19 -08:00
FreeScout
ec83995838 Add vendor files to dist 2025-01-24 05:58:27 -08:00
FreeScout
e71de79923 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2025-01-24 05:57:46 -08:00
FreeScout
20188aecb1 v1.8.168 2025-01-24 05:56:49 -08:00
FreeScout
b2abca9af1 Fix empty message issue - closes #4508 2025-01-24 05:51:55 -08:00
FreeScout
29491f663f Fix accents and special characters removed from new customer names - closes #4506 2025-01-23 22:58:43 -08:00
FreeScout
2d821be5e1 PHP 8.4 compatibility - closes #4501 2025-01-23 09:40:31 -08:00
FreeScout
08b8e1cf1d Fix Undefined variable $e in Header.php - closes #4505 2025-01-23 09:06:19 -08:00
FreeScout
e1a493acd8 Add vendor files to dist 2025-01-22 20:16:21 -08:00
FreeScout
c8c0db5cb9 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2025-01-22 20:14:50 -08:00
FreeScout
417bf07f69 v1.8.167 2025-01-22 20:14:41 -08:00
FreeScout
17b8eeae96 Fix Call to undefined function str_starts_with() on fetching with PHP 7.x - closes #4499 2025-01-22 20:12:56 -08:00
FreeScout
d4a92e9bb9 PHP 8.4 compatibility 2025-01-22 06:17:23 -08:00
FreeScout
020b9e7c18 Exclude Modules folder from composer classmap - closes #4496 2025-01-21 10:15:45 -08:00
FreeScout
9b0cec6b2b Add vendor files to dist 2025-01-21 03:27:14 -08:00
FreeScout
736e931529 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2025-01-21 03:25:26 -08:00
FreeScout
e84fd891bf v1.8.166 2025-01-21 03:25:14 -08:00
FreeScout
888701e066 Rollback PHPUnit to v9.5.28 as v11.2.6 does not work with PHP 7.4 2025-01-21 02:18:54 -08:00
FreeScout
b59144e704 Update GitHub tests 2025-01-21 02:12:32 -08:00
FreeScout
eae50903f2 Update GitHub tests 2025-01-21 02:05:19 -08:00
FreeScout
66e19c9b2c PHPUnit v11.2.6 2025-01-21 00:50:49 -08:00
FreeScout
dcdb7e68cf PHP 8.4 compatibility 2025-01-20 22:55:21 -08:00
FreeScout
a363fb264c Add latest PHP versions to GitHub tests 2025-01-20 22:41:15 -08:00
FreeScout
877813cb73 Fix Unexpected ClientManager in FixtureWebklexMessage.php 2025-01-20 22:38:38 -08:00
FreeScout
0ced99564d Replace password with asterisks in PopProtocol.php logs 2025-01-20 22:27:34 -08:00
FreeScout
d6db40d563 Webklex/php-imap: send NOOP connection check once in second 2025-01-20 22:16:04 -08:00
FreeScout
4591dec171 Update composer.json scripts 2025-01-20 21:55:13 -08:00
FreeScout
d8b7723a7c Few tests for Webklex/php-imap 2025-01-20 21:15:26 -08:00
FreeScout
c7576c52e4 Do not require --mailbox parameter in freescout:parse-eml command 2025-01-20 10:44:06 -08:00
FreeScout
678cd45198 Improve MailHelper::decodeSubject() function 2025-01-20 08:07:53 -08:00
FreeScout
4b1afadf92 Remove blockquote from the list of reply separators - closes #4460 2025-01-20 05:11:59 -08:00
FreeScout
0422b29321 Webklex/php-imap: Fixed attachments with sspecial symbols in filename 2025-01-20 01:39:47 -08:00
FreeScout
9989bf02d3 Webklex/php-imap: send NOOP connection check once in second 2025-01-19 23:49:18 -08:00
FreeScout
c35b7ff3c0 Webklex/php-imap: Decode partially encoded address names 2025-01-19 23:18:22 -08:00
FreeScout
38e22ef69c Update comment for IMAP BODY[TEXT] 2025-01-19 22:58:43 -08:00
FreeScout
d2b7a87bbb Webklex/php-imap: Boundary parsing fixed and improved to support more formats 2025-01-19 22:56:27 -08:00
FreeScout
3208a8c1d9 Webklex/php-imap: Address parsing improved and extended to include more cases 2025-01-19 22:41:40 -08:00
FreeScout
ab8309bdab Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2025-01-18 01:53:52 -08:00
FreeScout
12584db7b5 v1.8.165 2025-01-18 01:53:39 -08:00
FreeScout
6dac6eb1df Make \Webklex\PHPIMAP\Header::rfc822_parse_headers() function static 2025-01-17 21:01:59 -08:00
FreeScout
3a33fd4628 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2025-01-17 20:45:37 -08:00
FreeScout
e1b9d711d3 v1.8.164 2025-01-17 20:45:28 -08:00
FreeScout
9e1b53d410 Fix accessing array offset on null in Header.php - closes #4488 2025-01-17 09:20:08 -08:00
FreeScout
ac43d4628d Fix Waiting Since not showing - closes #4486 2025-01-17 08:44:52 -08:00
FreeScout
33a7d06032 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2025-01-17 02:00:23 -08:00
FreeScout
a110039125 v1.8.163 2025-01-17 02:00:02 -08:00
FreeScout
dd654551c1 Fix Waiting Since not showing - closes #4486 2025-01-17 01:36:34 -08:00
FreeScout
74048295fa Fix user notification subject missing - closes #4485 2025-01-17 01:06:30 -08:00
FreeScout
e5cfdf58af Webklex/php-imap: Better connection check for IMAP 2025-01-17 00:25:01 -08:00
FreeScout
1595be8c0d Webklex/php-imap: Query - Chunked - Resolved infinite loop when start chunk > 1 2025-01-16 23:48:41 -08:00
FreeScout
925f9f4bc8 Webklex/php-imap: Make the space optional after a comma separator 2025-01-16 23:43:52 -08:00
FreeScout
a3f49ca25c Webklex/php-imap: Fixed date issue if timezone is UT and a 2 digit year 2025-01-16 23:42:08 -08:00
FreeScout
177e54f1f2 Add vendor files to dist 2025-01-16 23:28:40 -08:00
FreeScout
7efa8f5a1c Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2025-01-16 23:26:40 -08:00
FreeScout
18a5efcc98 v1.8.162 2025-01-16 23:25:36 -08:00
FreeScout
da99de9a44 Helper::jsonEncodeSafe() function - closes #4471 2025-01-16 22:39:36 -08:00
FreeScout
2c8caef9a6 PHP 8.4 compatibility 2025-01-16 21:29:27 -08:00
FreeScout
63b435ac42 Add Asia/Colombo timezone to timezone options - closes #4483 2025-01-16 21:28:06 -08:00
FreeScout
898f1d3266 Allow to preserve transparency in Helper::resizeImage() - closes #4480 2025-01-16 00:49:27 -08:00
FreeScout
c92d52d055 Added filters allowing module customisation of email reply and user notification templates and data. 2025-01-16 00:12:02 -08:00
FreeScout
486e777210 Added filters allowing module customisation of email reply and user notification templates #4477 from vtq221/master
Added filters allowing module customisation of email reply and user notification templates
2025-01-16 09:10:46 +01:00
FreeScout
ef4bbef17e Implement POP3 protocol without PHP IMAP extension functions 2025-01-15 23:57:08 -08:00
FreeScout
54226692a0 Fix Call to a member function getFullName() on null when some user has been deleted from DB - closes #4467 2025-01-15 23:52:30 -08:00
FreeScout
0d5944b95a PHP 8.4 compatibility 2025-01-15 05:50:36 -08:00
FreeScout
4627ec463a Replace fread() with fgets() in Webklex to avoid possible infinite loops 2025-01-14 22:15:48 -08:00
FreeScout
ca0bc6ef2a Avoid PHP IMAP extension functions 2025-01-14 21:26:56 -08:00
FreeScout
e26f7c5e84 Update Swedish language 2025-01-14 21:21:58 -08:00
FreeScout
e2617dbbd8 PHP 8.4 compatibility 2025-01-14 09:41:27 -08:00
FreeScout
185d660f8b Avoid PHP IMAP extension functions 2025-01-14 09:40:42 -08:00
vtq
a264424afb Added filters allowing module customisation of email reply and user notification templates and data. 2025-01-14 23:58:26 +11:00
FreeScout
50f0f25e0b Avoid PHP IMAP extension functions 2025-01-14 02:36:01 -08:00
FreeScout
976b926d2d PHP 8.4 compatibility 2025-01-14 00:55:41 -08:00
FreeScout
b26fb0e13a Add vendor files to dist 2025-01-13 03:00:56 -08:00
FreeScout
fa46d45662 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2025-01-13 03:00:07 -08:00
FreeScout
c56d315eb5 v1.8.161 2025-01-13 02:59:24 -08:00
FreeScout
222ffece76 Do not remove customer phone when creating a phone conversation without specifying customer phone number - closes #4464 2025-01-13 02:25:06 -08:00
FreeScout
cb8d99fe5a Fix deleting attachments when creating a new conversation from existing thread - closes #4432 2025-01-13 02:10:18 -08:00
FreeScout
c771001d9f Review CVE-2024-6484 and CVE-2024-6485 in the Bootstrap JS 2025-01-13 01:51:31 -08:00
FreeScout
7d7f1e708e Allow to get out of quote block in the editor - closes #3811 2025-01-01 23:30:37 -08:00
FreeScout
978138df1a Fix incorrect year returned by User::dateFormat() - closes #4443 2024-12-30 22:02:57 -08:00
FreeScout
095af77472 Fix Implicit conversion from float to int error in Guzzle - closes #4439 2024-12-20 21:03:16 -08:00
FreeScout
5eaa427f09 Fix Call to a member function getFullName() on null in Thread.php - closes #4438 2024-12-20 08:42:14 -08:00
FreeScout
178ac6d19b Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2024-12-20 08:28:52 -08:00
FreeScout
18fb794c75 v1.8.160 2024-12-20 08:28:42 -08:00
FreeScout
1148a35663 Fix preserving attachments when creating a new conversation from existing thread - closes #4432 2024-12-19 01:30:52 -08:00
FreeScout
043b7dd5ce Allow class attribute for tables in purifier config - closes #4429 2024-12-17 23:54:27 -08:00
FreeScout
13bf22065f Improve parts boundary detection in Webklex/php-imap - closes #4417 2024-12-12 04:17:40 -08:00
FreeScout
0e14e087a5 Make Insert Variable dropdown accessible via screenreaders - closes #4412 2024-12-10 23:31:26 -08:00
FreeScout
d41fac7a7f Fix OnOffswitch checkboxes on screenreaders - closes #4412 2024-12-10 21:24:03 -08:00
FreeScout
6abf8cde9e Action toolbar changes and added a new filter - #4396 2024-12-10 01:58:56 -08:00
FreeScout
17b81de9e1 Action toolbar changes and added a new filter #4396 from Lars-/master
Action toolbar changes and added a new filter
2024-12-10 12:50:49 +03:00
Lars Jansen
066871b1a3 chore: changed filter hook name 2024-12-10 10:39:36 +01:00
FreeScout
2f2e010796 Add aria-label to the Notifications dropdown in navigation menu - #4412 2024-12-09 22:52:31 -08:00
FreeScout
1ab7806356 Add getLocale() function to main.js 2024-12-09 08:33:05 -08:00
Lars Jansen
88c00d5a02 fix: delete button 2024-12-09 13:59:30 +01:00
FreeScout
b887113b50 New hooks: listeners.failed_login and fetch_emails.set_seen_flag #4407 from vtq221/master
New hooks: listeners.failed_login and fetch_emails.set_seen_flag
2024-12-07 13:29:52 +01:00
FreeScout
51dbe56f3f Improve Helper::strSplitKeepWords() function with Chinese symbols 2024-12-07 04:26:23 -08:00
vtq
f4fea342ed Added filter (filter set seen flag for messages) and action (send action on failed login) to enable for use in module development (email customization and failed login alerting/firewall). 2024-12-07 19:06:22 +11:00
FreeScout
01bfd5c971 Update readme 2024-12-06 09:23:44 -08:00
FreeScout
2ccc2cbd66 Update readme 2024-12-05 08:12:49 -08:00
Lars Jansen
143d96af6b feat: added filter on mailboxes in move_conv popup 2024-12-04 11:56:11 +01:00
Lars Jansen
93391f826c feat: make toolbar and dropdown buttons dynamic so they can be rearranged more easily 2024-12-04 11:56:00 +01:00
FreeScout
0790a5114f Update Dutch translation 2024-12-02 02:29:02 -08:00
FreeScout
11058803c7 Add extra phrase to no_retry_mail_errors list - closes #870 2024-12-02 00:57:59 -08:00
FreeScout
df0ab1add9 Fix vars.js broken when custom translation contains quotes - closes #4369 2024-11-29 22:21:21 -08:00
FreeScout
b564d20627 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2024-11-29 20:45:28 -08:00
FreeScout
3562298031 v1.8.159 2024-11-29 20:45:00 -08:00
FreeScout
7706d52e63 Fix text bolding issue - closes #4367 2024-11-29 06:31:14 -08:00
FreeScout
ff2e92f1ff Add layout.title.name hook 2024-11-29 06:18:39 -08:00
FreeScout
4b2f385e43 Fix Spanish translation #4376 from anxodio/patch-2
Fix Spanish translation
2024-11-28 17:17:59 +01:00
Àngel Fernández
a15c29bea0 Fix bad translation
The translation says the opposite than the original intention
2024-11-28 16:10:32 +01:00
FreeScout
a0594bf6d9 Fix Undefined array key 2 error on fetching - closes #4372 2024-11-28 04:43:24 -08:00
FreeScout
6e0a213003 Fix text bolding issue - closes #4367 2024-11-28 04:30:42 -08:00
FreeScout
8024c72db5 Optimize UpdateFolderCounters Job #4374 from mahavirn/optimize-update-folder-count-job
Optimize UpdateFolderCounters Job
2024-11-28 11:50:10 +03:00
Mahavir Nahata
ecc9ea9ab9 Optimize UpdateFolderCounters Job 2024-11-28 13:38:06 +05:30
FreeScout
b3dbad9a6e Show more detailed errors on fetching - closes #4372 2024-11-27 04:09:16 -08:00
FreeScout
ccdbd265c8 Show more detailed errors on fetching - closes #4372 2024-11-27 01:09:34 -08:00
FreeScout
a0659a8971 Add Precedence:list to the list of no auto-reply send headers - closes #4363 2024-11-24 23:01:19 -08:00
FreeScout
ef4d37f308 Fix text bolding issue - closes #4367 2024-11-24 22:08:36 -08:00
FreeScout
a49fd0ad33 Clear cache manually in update.sh to avoid post-update issues - closes #4366 2024-11-24 02:06:17 -08:00
FreeScout
190d2daece Fix Argument 1 (lastErrors) must be of type array in Carbon 2024-11-23 20:58:26 -08:00
FreeScout
bfbfeb2e66 Add vendor files to dist 2024-11-22 00:37:13 -08:00
FreeScout
a11df91aa4 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2024-11-22 00:34:55 -08:00
FreeScout
4d51348487 v1.8.158 2024-11-22 00:34:40 -08:00
FreeScout
0e7e1a54f4 Fix an issue with the SPAN tag being added in the editor - closes #4354 2024-11-17 09:35:13 -08:00
FreeScout
b7938df35b Do not check In-Reply-To header when processing @fwd instruction - closes #4348 2024-11-15 02:24:52 -08:00
FreeScout
b622bbb284 Remove Webklex/laravel-imap library 2024-11-15 02:14:50 -08:00
FreeScout
009c51e245 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2024-11-14 23:36:24 -08:00
FreeScout
21fe296303 v1.8.157 2024-11-14 23:36:13 -08:00
FreeScout
2a0200f46d Fix table wizard in the editor - closes #4345 2024-11-14 00:15:41 -08:00
FreeScout
f5971fb24f Adjust placeholder in translations - closes #4346 2024-11-13 23:39:12 -08:00
FreeScout
76c550ff99 Croatian translation #4346 from shunkica/hr-translation
Croatian translation
2024-11-14 08:23:39 +01:00
shunkica
2d1fec3cb8 Update hr.json 2024-11-13 23:11:37 +01:00
shunkica
097f10ec18 Update croatian translations 2024-11-13 23:06:59 +01:00
FreeScout
b63bad55ac Allow to set APP_FETCHING_BUNCH_SIZE env parameter - closes #4343 2024-11-13 05:26:10 -08:00
FreeScout
94c1c01480 Do not reset Default Redirect settings for users when saving Mailbox Permissions - closes #4339 2024-11-12 05:02:27 -08:00
FreeScout
21053200f0 Add conversations.table_sorting hook - closes #4338 2024-11-12 02:01:40 -08:00
FreeScout
8d65bdf0bf Fix @fwd forwarding when styles are present in the email body - closes #4333 2024-11-11 04:23:28 -08:00
FreeScout
e2e3682ca8 Update readme 2024-11-11 00:55:31 -08:00
FreeScout
fc7f4bfc35 Fix moving emails to a folder on fetching on mail servers not supporting MOVE command - closes #4313 2024-11-07 21:14:55 -08:00
FreeScout
e2360f2146 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2024-11-04 08:24:58 -08:00
FreeScout
910edcaea2 v1.8.156 2024-11-04 08:24:43 -08:00
FreeScout
ffcce029f5 Fix Undefined variable customer in thread.blade.php - closes #4321 2024-11-04 08:22:41 -08:00
FreeScout
b6987ddb90 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2024-11-01 21:45:43 -07:00
FreeScout
cbeffb850d v1.8.155 2024-11-01 21:45:35 -07:00
FreeScout
bbcba6e266 Allow to hide from support agents customers from non-visible mailboxes - closes #4316 2024-11-01 10:13:41 -07:00
FreeScout
9a6429ea97 Allow to hide from support agents customers from non-visible mailboxes - closes #4316 2024-11-01 00:35:14 -07:00
FreeScout
52e5ecf905 Update German translation 2024-10-29 03:03:18 -07:00
FreeScout
2912422a31 Fix typo in user profile.blade.php 2024-10-27 10:55:21 -07:00
FreeScout
e17da2b6c0 Update Japanese translation 2024-10-26 07:16:11 -07:00
FreeScout
b276cea24a Fix error in Thread->getFromHeader() 2024-10-26 02:55:53 -07:00
FreeScout
e7ec7e7364 Set CURLOPT_FOLLOWLOCATION in Helper::getRemoteFileContents() 2024-10-26 02:52:51 -07:00
FreeScout
46add74e4f Show From address in the conversation when From and Reply-To headers are different in the email - closes #4287 2024-10-18 01:38:33 -07:00
FreeScout
4e8cd07303 Fix decoding Japanese symbols in the email body in gb2312 encoding - closes #4282 2024-10-16 22:50:08 -07:00
FreeScout
5db8906a86 Add hooks: conversation.convinfo.prepend and conversation.convinfo.before_nav - closes #4279 2024-10-08 22:46:52 -07:00
FreeScout
927afe44c7 Add Delivered-To:Autoresponder to the list of autoresponder headers 2024-10-04 11:07:41 -07:00
FreeScout
30b07a82c7 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2024-10-04 04:34:21 -07:00
FreeScout
c80ce3d4f8 v1.8.154 2024-10-04 04:34:11 -07:00
FreeScout
980035fecc Improve the email search by Message-Id in Show Original window - closes #4266 2024-10-04 04:31:47 -07:00
FreeScout
6af389655c Check if user is deleted on Permissions and Notifications pages 2024-10-03 00:52:20 -07:00
FreeScout
eb5c8fac12 Create User::deleteUser() method 2024-10-03 00:26:33 -07:00
FreeScout
3f90a9bad3 Improve connection sleep time selection when fetching emails to avoid connection setup failed error - closes #4227 2024-10-02 03:53:10 -07:00
FreeScout
0280e4f30d Fix SMTP authentication not working in some cases due to setAuthMode() used in Swiftmailer - closes #4250 2024-10-02 03:34:58 -07:00
FreeScout
b92e502fb0 Fix null given error in getMessageNumber() - closes #4257 2024-09-27 07:00:31 -07:00
FreeScout
8cb8a180b7 Fix null given error in getMessageNumber() - closes #4257 2024-09-27 06:58:39 -07:00
FreeScout
5c9ab337ea Add vendor files to dist 2024-09-22 07:01:39 -07:00
FreeScout
f6e18818cd Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2024-09-22 07:00:04 -07:00
FreeScout
880205fc59 v1.8.153 2024-09-22 06:33:42 -07:00
FreeScout
c073511378 Update German translation 2024-09-22 06:17:35 -07:00
FreeScout
275454d479 Add CSRF check to the Delete all files link in Manage App Logs 2024-09-20 05:57:16 -07:00
FreeScout
a96b31fb99 Avoid process list trancation when checking running commands - closes #4237 2024-09-20 02:58:03 -07:00
FreeScout
a19bfd421c Improve connection sleep time selection when fetching emails to avoid connection setup failed error - closes #4227 2024-09-12 23:53:52 -07:00
FreeScout
fbae84ba3c Improve connection sleep time selection when fetching emails to avoid connection setup failed error - closes #4227 2024-09-12 22:30:16 -07:00
FreeScout
e63904a53b Add X-Autoresponder to the list of auto-responder headers - closes #4226 2024-09-12 00:19:38 -07:00
FreeScout
5a989ec257 Improve @fwd detection when forwarding emails - closes #2940 2024-09-11 10:12:58 -07:00
FreeScout
b90f3d7204 Change /bin/sh to /bin/bash in update.sh - closes #4208 2024-09-03 00:54:17 -07:00
FreeScout
03c234edec Fix an issue with emails being empty when fetching from iCloud - closes #4202 2024-08-29 00:42:12 -07:00
FreeScout
af2b1254e8 Update Polish translation 2024-08-28 23:52:21 -07:00
FreeScout
f218fe17d0 Add vendor files to dist 2024-08-27 23:15:39 -07:00
FreeScout
a2979a4406 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2024-08-27 23:15:16 -07:00
FreeScout
e4dd3b4ad1 v1.8.152 2024-08-27 23:15:04 -07:00
FreeScout
aa713a3a57 Fix an issue with plain text emails having empty body - closes #4181 2024-08-27 23:13:45 -07:00
FreeScout
adc9921108 Add vendor files to dist 2024-08-27 04:52:34 -07:00
FreeScout
f326b14776 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2024-08-27 04:52:00 -07:00
FreeScout
afaf1f53ab v1.8.151 2024-08-27 04:50:57 -07:00
FreeScout
23bb50eb86 Fix out of memory on Status page when there are many background jobs - closes #4197 2024-08-27 03:58:19 -07:00
FreeScout
39824c876d Fix Undefined array key 1 error on fetching emails - closes #4195 2024-08-26 22:22:06 -07:00
FreeScout
ca1a7d0900 Fix non-multipart emails contain headers in the bodies - closes #4181 2024-08-24 03:02:23 -07:00
FreeScout
351eff9b80 Fix checking POP3 connection when PHP Request Shutdown: Unexpected characters at end of address error occurs 2024-08-23 22:48:46 -07:00
FreeScout
e037011c55 Added Kazakh language 2024-08-16 09:10:07 -07:00
FreeScout
c0ce07a2b9 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2024-08-15 20:10:13 -07:00
FreeScout
94f11a9155 v1.8.150 2024-08-15 20:09:47 -07:00
FreeScout
dff234ffbe Allow to disable quotes around SINCE date in IMAP search query via APP_SINCE_WITHOUT_QUOTES_ON_FETCHING env parameter - closes #4175 2024-08-15 20:07:55 -07:00
FreeScout
223b4d12dc Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2024-08-14 09:51:38 -07:00
FreeScout
b33fb5e1d3 v1.8.149 2024-08-14 09:51:26 -07:00
FreeScout
3511e7acb2 Fix configuring MS365 SMTP via oAuth - closes #4179 2024-08-13 22:23:07 -07:00
FreeScout
be4a1234c3 Improve Webklex/php-imap memory usage 2024-08-13 11:24:45 -07:00
FreeScout
823014d4e9 Call parseRawBody() in Webklex Message if body has not been parsed yet 2024-08-13 11:08:16 -07:00
FreeScout
310d3290fe Do not add quotes around the date in IMAP SINCE query - closes #4175 2024-08-11 03:44:03 -07:00
FreeScout
07765cf8ff Populate variables in custom Mailbox names inside emails sent to customers - closes #4169 2024-08-08 05:00:40 -07:00
FreeScout
c4a6585cc7 Add vendor files to dist 2024-08-08 04:17:10 -07:00
FreeScout
d99d991650 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2024-08-08 04:16:35 -07:00
FreeScout
6e8682e693 v1.8.148 2024-08-08 04:15:59 -07:00
FreeScout
87eab76574 Set MS365 SMTP credentials automatically only when outlook.office365.com SMTP host is set - closes #4151 2024-08-08 04:00:35 -07:00
FreeScout
a011948149 Make toSql() comparison compatible with PostgreSQL - closes #4167 2024-08-07 03:56:31 -07:00
FreeScout
b9391d87dc Leave other emails unread when an exception happens during fetching of one of them - closes #4159 2024-08-02 23:32:49 -07:00
FreeScout
7e7f0a0d10 Leave other emails unread when an exception happens during fetching of one of them - closes #4159 2024-08-01 23:34:38 -07:00
FreeScout
8e62d70986 Correctly processs invalid dates when fetching emails - closes #4159 2024-08-01 21:22:33 -07:00
FreeScout
02e5fa8535 Correctly process invalid mail Reply-To headers - closes #4158 2024-08-01 21:01:42 -07:00
FreeScout
2dc8d061e1 Prefill SMTP username and password for MS365 if those fields are empty - closes #4151 2024-07-30 22:21:34 -07:00
FreeScout
efca2acbe2 Log Helper::getRemoteFileContents() errors 2024-07-22 09:50:01 -07:00
FreeScout
029e5c8a1f Execute hooks when performing conversations bulk delete to Deleted folder - closes #4141 2024-07-22 06:30:26 -07:00
FreeScout
7383b95864 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2024-07-21 06:19:34 -07:00
FreeScout
5be99f03f3 v1.8.147 2024-07-21 06:19:27 -07:00
FreeScout
da522d8d58 Fix Disconnect link for MS365 in Fetching settings - closes #4136 2024-07-18 02:30:49 -07:00
FreeScout
60ac03df04 Improve App Key check in tools.php 2024-07-17 09:38:38 -07:00
FreeScout
5a22011fe6 Improve sanitizing Path to PHP in tools.php 2024-07-17 09:37:21 -07:00
FreeScout
09f4908def Fix Call to a member function getFirstName() on null in notification.blade.php - closes #4132 2024-07-16 22:17:20 -07:00
FreeScout
8172db9cdb Improve sanitizing Path to PHP in tools.php 2024-07-16 22:12:57 -07:00
FreeScout
05e7e1e739 Improve sanitizing Path to PHP in tools.php 2024-07-16 07:03:27 -07:00
FreeScout
b2c72775dd Merge pull request #4130 from IchHabeHunger54/master
Add conversation->changeSubject() and conversation.subject_changed action
2024-07-16 13:55:52 +03:00
Neustifter, David
9eb7457097 Add Conversation->changeSubject() and conversation.subject_changed action 2024-07-16 12:47:07 +02:00
FreeScout
0b7d1dc06f Allow padding-left and simiar CSS properties when purifying HTML 2024-07-11 00:53:42 -07:00
FreeScout
2f9d1bf92e Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2024-07-08 01:44:11 -07:00
FreeScout
de405fe60f v1.8.146 2024-07-08 01:44:01 -07:00
FreeScout
9bb3ee9bc1 Use only forward slashes in attachment URLs - closes #4106 2024-07-04 21:04:52 -07:00
FreeScout
5ab6a42808 Fallback to getPathname() for uploaded files - closes #4105 2024-07-04 21:03:14 -07:00
FreeScout
9b5286efca Fallback to getPathname() for uploaded files - closes #41054105 2024-07-04 20:59:32 -07:00
FreeScout
78e1067c07 Do not duplicate recipient in conversation CC field - closes #4112 2024-07-03 08:24:55 -07:00
FreeScout
c19c4ee217 Fix conversation draft not visible in Drafts after undoing new conversation - closes #4112 2024-07-03 07:55:25 -07:00
FreeScout
5ef26eaf32 Fix error message when can not fetch specific email vai MailHelper::fetchMessage() - closes #4097 2024-07-03 04:37:07 -07:00
FreeScout
16843c9d6a Fix Datatables sort_desc.png not found - closes #4107 2024-07-03 00:45:22 -07:00
FreeScout
a21f5c9d53 Merge branch 'master' of github.com:freescout-helpdesk/freescout 2024-07-02 12:02:26 -07:00
FreeScout
97cea6cdef Fix Undefined array key auth_mode on sending emails - closes #4103 2024-07-02 12:02:09 -07:00
FreeScout
12c6552259 Merge pull request #4099 from gabllux/master
Handle null values on outOauthEnabled() - closes #4094
2024-07-02 20:54:56 +02:00
gabllux
04aaeb4cc1 Handle null values on outOauthEnabled() - closes https://github.com/freescout-help-desk/freescout/issues/4094 2024-06-25 21:33:01 +02:00
FreeScout
7c3e57aa57 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2024-06-19 11:39:48 -07:00
FreeScout
f90ba3d6d6 v1.8.145 2024-06-19 11:39:29 -07:00
FreeScout
4fe2a9333e Improve SMTP oAuth implementation - closes #4084 2024-06-19 09:47:03 -07:00
FreeScout
bd59fa5ef5 Mention posix_isatty() function in \Helper::checkRequiredFunctions() 2024-06-19 04:24:00 -07:00
FreeScout
989c729834 Add created_at index to threads table - closes #4069 2024-06-18 02:26:10 -07:00
FreeScout
a57a424067 Update readme 2024-06-18 00:46:15 -07:00
FreeScout
755e4f0cd7 Add vendor files to dist 2024-06-17 22:12:38 -07:00
FreeScout
a2a0539316 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2024-06-17 22:11:43 -07:00
FreeScout
e8975d04d6 v1.8.144 2024-06-17 22:11:34 -07:00
FreeScout
d6eac44275 Show more detailed errors when IMAP authentication fails 2024-06-17 10:54:23 -07:00
FreeScout
ea39e24727 Rename STARTTLS Encryption method in Fetching settings 2024-06-17 06:31:18 -07:00
FreeScout
5a290b3ad3 Allow to enable debug in Status - Tools - Fetching section 2024-06-16 02:21:06 -07:00
FreeScout
ce523c51e7 Show debug log when testing fetching 2024-06-16 02:05:42 -07:00
FreeScout
5bcde678d1 Update repo URL in self-updater 2024-06-15 08:30:44 -07:00
FreeScout
98e74e53f2 Update repo link 2024-06-14 00:36:27 -07:00
FreeScout
bde2da820c Update repo URL in self-updater 2024-06-14 00:06:10 -07:00
FreeScout
4b2dd40d99 Update also inactive modules via freescout:module-update command 2024-06-13 03:22:17 -07:00
FreeScout
4d7f8d1f50 Microsoft Exchange oAuth for SMTP - closes #4072 2024-06-12 22:21:03 -07:00
FreeScout
8510ee8224 Microsoft Exchange oAuth for SMTP - closes #4072 2024-06-12 20:45:58 -07:00
FreeScout
7e8e514e7b Microsoft Exchange oAuth for SMTP - closes #4072 2024-06-12 11:45:08 -07:00
FreeScout
fb3b0d3a49 Update readme 2024-06-11 22:18:50 -07:00
FreeScout
aaacc6d3fa Fix Call to undefined function get_current_user() error on Status page - closes #4067 2024-06-10 22:31:29 -07:00
FreeScout
3c199f697c Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2024-06-10 10:25:57 -07:00
FreeScout
5c01c3793c v1.8.143 2024-06-10 10:25:45 -07:00
FreeScout
0ff901ef32 Fix Undefined variable $customer in thread.php - closes #4066 2024-06-10 10:24:36 -07:00
FreeScout
08a5d5f6ff Add MailHelper::parseEml() function 2024-06-10 04:42:42 -07:00
FreeScout
1fd00def95 Allow to call \Webklex\PHPIMAP\Message::make() without specifying uid 2024-06-10 01:27:24 -07:00
FreeScout
21a73fa5e6 Add vendor files to dist 2024-06-09 09:37:24 -07:00
FreeScout
c9b2c3cc36 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2024-06-09 09:36:57 -07:00
FreeScout
4ee876c134 v1.8.142 2024-06-09 09:36:48 -07:00
FreeScout
c1c713ef15 Show From for emails received from customers with multiple emails - closes #4059 2024-06-06 11:52:59 -07:00
FreeScout
b926cd5cf2 Do not show Mailbox is empty error when fetching via POP3 - closes #4060 2024-06-06 11:44:21 -07:00
FreeScout
325343c2cf Fix sending a new conversation to the customer with multiple emails - closes #4057 2024-06-05 04:00:26 -07:00
FreeScout
d9502ff348 Fix error in module:list command - closes #4056 2024-06-05 00:00:37 -07:00
FreeScout
585f8f49a7 Sort modules list in the Deactivate License dialog - closes #4055 2024-06-03 22:45:54 -07:00
FreeScout
505db999ad Fix mb_convert_encoding() invalid encoding error on fetching - closes #4051 2024-06-01 08:55:33 -07:00
FreeScout
d402dd5164 Catch mb_convert_encoding() error on fetching - closes #4051 2024-06-01 08:36:57 -07:00
FreeScout
a93cc2a08f Improve Gmail reply separation on fetching 2024-06-01 03:34:03 -07:00
FreeScout
0ac8685c63 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2024-05-30 04:55:43 -07:00
FreeScout
754b2fc40c v1.8.141 2024-05-30 04:55:26 -07:00
FreeScout
c32a68ed93 Remove All from current mailbox link from conversation page - closes #4041 2024-05-27 01:30:51 -07:00
FreeScout
c269311ef5 Skip auto-replies sent to the email notification on behalf of a user - #4035 2024-05-27 01:24:35 -07:00
FreeScout
04c28a9637 Disable Check Connection button on changing mailbox Fetching settings 2024-05-22 22:33:24 -07:00
FreeScout
6830a78e23 Show exclamation icon in mailbox connection settings tabs 2024-05-22 22:04:29 -07:00
FreeScout
3a4921344b Show exclamation icon in mailbox connection settings tabs 2024-05-22 22:02:03 -07:00
FreeScout
063b77f3df Fix IMAP folders not being retrieved in some cases 2024-05-22 21:50:26 -07:00
FreeScout
61e5f1fde5 Fix fetching emails forwarded using @fwd command - closes #4036 2024-05-21 11:36:15 -07:00
FreeScout
4f253bfab1 Fix reteiving the original message in Show Original window for moved conversations 2024-05-21 10:33:16 -07:00
FreeScout
c88ca44ff1 Find prev_thead iteratively looking in-reply-to and references #4026 from Som-Energia/master
Find prev_thead iteratively looking in-reply-to and references
2024-05-16 09:01:22 +03:00
FreeScout
2d871f2759 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2024-05-15 22:57:28 -07:00
FreeScout
2a0bac500c v1.8.140 2024-05-15 22:57:20 -07:00
FreeScout
cedf4de2ba Update TestWebklex command 2024-05-15 22:46:16 -07:00
Àngel Fernández
d9e1a7ae98 Find prev_thead iteratively looking in-reply-to and references 2024-05-15 18:15:31 +02:00
FreeScout
80118d579d Update general_help_request.md 2024-05-15 07:13:47 -07:00
FreeScout
0299c8be99 Switch to the new fetching library 2024-05-10 22:46:33 -07:00
FreeScout
70a7ec54ea Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2024-05-10 22:15:09 -07:00
FreeScout
4876d37287 v1.8.139 2024-05-10 22:14:53 -07:00
FreeScout
9894ca78f0 app.csp_custom env parameter 2024-05-09 06:34:05 -07:00
FreeScout
99a4b4b4e1 Remove unwanted tags from HTML when editing threads - GHSA-985r-6qfc-hg8m 2024-05-06 00:52:36 -07:00
FreeScout
1169f5bae9 Strip unwanted tags from mailbox Auto Reply 2024-05-06 00:16:53 -07:00
FreeScout
a59619c153 Strip unwanted tags from mailbox signature when saving it 2024-05-06 00:14:00 -07:00
FreeScout
c4932c4c8e Fix CC and BCC fields when cloning a conversation - closes #4011 2024-05-05 09:44:35 -07:00
FreeScout
cb31699a6e Update Polish translation 2024-05-05 00:08:47 -07:00
FreeScout
41e6e10241 Update Polish translation 2024-05-05 00:07:28 -07:00
FreeScout
ce5c85f592 Remove FORM tags from HTML when editing threads - GHSA-985r-6qfc-hg8m 2024-05-04 23:15:05 -07:00
FreeScout
2614514bc6 Do not allow Prototype Pollution via getQueryParam() function - GHSA-rx6j-4c33-9h3r 2024-05-04 22:59:35 -07:00
FreeScout
a50c261f13 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2024-04-26 06:41:18 -07:00
FreeScout
b474193f97 v1.8.138 2024-04-26 06:41:10 -07:00
FreeScout
27a4f2e8ff Fix cron URL link - closes #4000 2024-04-26 03:53:37 -07:00
FreeScout
17024f814a Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2024-04-22 09:35:48 -07:00
FreeScout
d18cc23149 v1.8.137 2024-04-22 09:35:36 -07:00
FreeScout
047111ffbb Fix Unknown hashing algorithm crc32c error in Webklex/php-imap library - closes #3991 2024-04-22 06:51:49 -07:00
FreeScout
8c5dbb4197 Fetch emails sent to multiple mailboxes on a per mailbox basis - closes #3941 2024-04-22 01:36:46 -07:00
FreeScout
7a6cbab8af Do not show closed conversations in non-standard folders - closes #3988 2024-04-20 23:17:20 -07:00
FreeScout
add6f179a4 Add folder.only_assigned_condition hook - closes #701 2024-04-20 22:27:13 -07:00
FreeScout
29d8c41395 Fix browser push notifications sent not to all users - closes #3880 2024-04-19 07:31:00 -07:00
FreeScout
0ecfa248ee Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2024-04-18 23:35:34 -07:00
FreeScout
748c7c5de1 v1.8.136 2024-04-18 23:34:12 -07:00
FreeScout
2b5be510b7 Update Dutch translation 2024-04-16 06:34:24 -07:00
FreeScout
99ef4e6899 Do not run scheduled commands when running \Artisan::call() function - closes #3970 2024-04-15 07:17:25 -07:00
FreeScout
59b86a882e Fix attachments not being shown when re-opening a new conversation draft - closes #3957 2024-04-12 22:12:10 -07:00
FreeScout
a6a729189c Show system date and time on Status page instead of current user time 2024-04-12 22:03:48 -07:00
FreeScout
7829e9a3ba Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2024-04-12 21:53:28 -07:00
FreeScout
a2e27f3ced v1.8.135 2024-04-12 21:53:13 -07:00
FreeScout
9175ce3646 Fix Parse error in Attachment.php - closes #3965 2024-04-12 21:52:35 -07:00
FreeScout
0dbdf401eb Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2024-04-12 11:20:10 -07:00
FreeScout
db84078093 v1.8.134 2024-04-12 11:19:57 -07:00
FreeScout
a0ab8069d2 Fix Header::decode() is not supported error in Webklex/php-imap - closes #3960 2024-04-12 11:18:45 -07:00
FreeScout
ea068dc23e Update Readme 2024-04-11 23:33:16 -07:00
FreeScout
26756cc687 Add vendor files to dist 2024-04-11 23:17:54 -07:00
FreeScout
171a4e3b48 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2024-04-11 23:17:21 -07:00
FreeScout
bfd5aef4d9 v1.8.133 2024-04-11 23:17:11 -07:00
FreeScout
b3c2a48eec Fix an error when new conversation draft is being discarded by other user - closes #3951 2024-04-11 23:15:26 -07:00
FreeScout
848cc9e43f Fix To field not appearing after discarding forwarding draft - closes #3950 2024-04-10 23:10:27 -07:00
FreeScout
f61230ff35 Update Polish translation 2024-04-10 22:54:15 -07:00
FreeScout
268740a142 Fix an issue with removing attachments when replying 2024-04-10 06:40:48 -07:00
FreeScout
abf8d32ff5 When forwarding a conversation do not include attachments from drafts 2024-04-10 06:39:15 -07:00
FreeScout
b769aec7e8 Fix attachments not being sent when forwarding a conversation 2024-04-10 06:29:10 -07:00
FreeScout
84832f011a Fix attachments being duplicated when discarding forwarding draft - closes #3949 2024-04-10 06:28:34 -07:00
FreeScout
b1dc766ffb Make updates in Webklex/php-imap library 2024-04-10 05:35:06 -07:00
FreeScout
3274b35468 Do not mark thread as containing attachments when only embedded images are present in the email 2024-04-09 07:06:47 -07:00
FreeScout
ad76e2d3ce Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2024-04-09 03:19:17 -07:00
FreeScout
aec78d147a v1.8.132 2024-04-09 03:19:03 -07:00
FreeScout
9333111a99 Fix some emails shown incorrectly - closes #3907 2024-04-08 11:21:52 -07:00
FreeScout
1f0d192fd5 Fix Call to a member function getFullName() on null error in notification template - closes #3942 2024-04-07 09:27:04 -07:00
FreeScout
21f04259cb Fix Call to a member function getFullName() on null error in notification template - closes #3942 2024-04-07 09:10:29 -07:00
FreeScout
a62eb20f26 Update ru.json 2024-04-06 23:16:01 -07:00
FreeScout
b813a4af9d Update ru.json #3945 from ViktorOn/master
Update ru.json
2024-04-07 08:51:13 +03:00
Vik
09ca411903 Update ru.json
Lots of edits
2024-04-07 00:37:14 +03:00
FreeScout
59c256f8cc Fix duplicated folder names when getting IMAP folders - closes #3937 2024-04-05 07:15:56 -07:00
FreeScout
c0e2a8a6fb Fix Passing null error in symfony/http-foundation/Request.php - closes #3940 2024-04-05 04:23:26 -07:00
FreeScout
c15278d3c6 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2024-04-04 22:21:07 -07:00
FreeScout
7cf3c67432 v1.8.131 2024-04-04 22:20:55 -07:00
FreeScout
1ef3796709 Fix attachment IDs when checking max message size - closes #3939 2024-04-04 11:24:33 -07:00
FreeScout
d3527f2aee Allow limiting command freescout:fetch-emails to specific mailboxes #3932 from ohader/fetch-emails-mailbox
Allow limiting command freescout:fetch-emails to specific mailboxes
2024-04-03 21:10:58 +03:00
Oliver Hader
31b3dff98e Allow limiting command freescout:fetch-emails to specific mailboxes
This change introduces an optional command option `mailboxes`, which
can be used to limit fetching messages to particular mailbox ids.

The following example fetches all messages of the last 500 days
for the specific mailboxes 5 and 7 only. By omitting the new
`mailboxes` option, any message of any mailbox would have been fetched.

```
php artisan freescout:fetch-emails --mailboxes=5,7 --unseen=0 --days=500
```
2024-04-03 20:06:51 +02:00
FreeScout
1b9cf02f1a Fix saving into Sent IMAP folder message with attachments - closes #3934 2024-04-03 10:06:38 -07:00
FreeScout
a9963c2f43 Fix saving into Sent IMAP folder message with attachments - closes #3934 2024-04-03 01:19:22 -07:00
FreeScout
b9bdf92065 Update Polish translation 2024-04-01 23:05:26 -07:00
FreeScout
b66b8f1880 Fix To and From headers when saving outgoing emails to the Sent IMAP folder - closes #3900 2024-04-01 23:04:01 -07:00
FreeScout
356b211754 Allow fetching seen/read messages via scheduler #3931 from ohader/schedule-fetch-unseen
Allow fetching seen/read messages via scheduler
2024-04-02 08:48:26 +03:00
FreeScout
833389c1fc Fix retrieving IMAP subfolders - closes #3933 2024-04-01 22:46:13 -07:00
Oliver Hader
f5e30cdff3 Allow fetching seen/read messages via scheduler
Usually the `schedule:run` command invokes `freescout:fetch-emails`,
but only fetches unseen (not read by anybody else, yet). This change
allows to configure this behavior and to fetch seen/read messages.

The new option `app.fetch_unseen` can be configured via `.env`:

```
APP_FETCH_UNSEEN=0
```
2024-04-01 14:54:58 +02:00
FreeScout
50f6cd743a Make params optional in Helper::setGuzzleDefaultOptions() #3926 from Lars-/master
Make params optional in Helper::setGuzzleDefaultOptions()
2024-03-30 08:10:49 +03:00
Lars Jansen
c4c6e861b5 fix: optional params in setGuzzleDefaultOptions 2024-03-30 06:02:34 +01:00
FreeScout
cac293b75b Use encrypted attachment IDs when uploading attachments 2024-03-29 09:37:56 -07:00
FreeScout
2c39ba9d9e Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2024-03-29 07:36:35 -07:00
FreeScout
a1602fa57a v1.8.130 2024-03-29 07:36:25 -07:00
FreeScout
f6f1adc9f4 Use encrypted attachment IDs when uploading attachments 2024-03-29 07:35:38 -07:00
FreeScout
9fbbba817e Fix conv_type_custom error - closes #3923 2024-03-29 07:34:49 -07:00
FreeScout
d973a8a2ca Update Japanese translation 2024-03-29 06:41:48 -07:00
FreeScout
70cc044e57 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2024-03-29 06:36:58 -07:00
FreeScout
f7498e5ba2 v1.8.129 2024-03-29 06:36:06 -07:00
FreeScout
e103599984 Helper encrypt and decypt with password functions 2024-03-29 06:34:40 -07:00
FreeScout
40abe0b311 Update Japanese translation 2024-03-28 23:02:19 -07:00
FreeScout
8b9e514ce9 Add Custom conversation type #3918 from Lars-/master
Add Custom conversation type
2024-03-28 11:26:34 +03:00
Lars Jansen
582f3a6ae4 feat: new conversation type: custom 2024-03-28 09:08:41 +01:00
Lars Jansen
5589c0a80f Merge remote-tracking branch 'Github/master' 2024-03-28 09:08:10 +01:00
FreeScout
71c67ad637 Re-sort strings in translation files 2024-03-26 23:07:15 -07:00
FreeScout
1c7aff0d5b Fix deleting conversations after sorting them - closes #3901 2024-03-25 23:50:32 -07:00
FreeScout
9b359abcce Add sender name to From field when saving email to the Sent folder - closes #3900 2024-03-25 00:24:28 -07:00
FreeScout
67de1109fe Add vendor files to dist 2024-03-22 00:11:37 -07:00
FreeScout
3489567cde Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2024-03-22 00:11:04 -07:00
FreeScout
d69e6c3501 v1.8.128 2024-03-22 00:10:55 -07:00
FreeScout
01dec5160e Exclude duplicate INBOX. folders when retreiving folders 2024-03-21 23:40:54 -07:00
FreeScout
90c80c9946 Fix removing comments when purifying HTML - closes #3894 2024-03-21 02:48:15 -07:00
FreeScout
0b9ca610b7 Fix Passing null to parameter error in HTMLPurifier - closes #3893 2024-03-20 23:07:07 -07:00
FreeScout
2a2782bc95 Fix conversations bulk actions when page parameter is set in the URL 2024-03-19 23:18:02 -07:00
FreeScout
3b6b02ed19 Fix conversation bulk actions after sorting conversations - closes #3884 2024-03-19 23:08:38 -07:00
FreeScout
443ae7f40b Fix long fetch command being killed - closes #3844 2024-03-19 06:45:05 -07:00
FreeScout
4c91ac44e1 Sanitize Path to PHP in tools.php 2024-03-19 04:47:43 -07:00
FreeScout
42dc98fa45 Strip script tags from mailbox signature 2024-03-19 03:15:37 -07:00
FreeScout
e88bec86fb Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2024-03-18 00:25:56 -07:00
FreeScout
20c8ef7383 v1.8.127 2024-03-18 00:25:48 -07:00
FreeScout
ff09db6244 Upgrade DataTables to 1.13.11 2024-03-18 00:11:45 -07:00
FreeScout
c15a00d603 Properly process situation when an attachment can not be saved to the disc - closes #3871 2024-03-13 07:14:24 -07:00
FreeScout
b18cf31bdc Show extended editor toolbar when editing threads - closes #3866 2024-03-12 00:37:51 -07:00
FreeScout
3ee1a96bde Allow <center> element when purifying thread content 2024-03-12 00:27:39 -07:00
FreeScout
8c470d0e4e Remove [if mso] from thread content when purifying 2024-03-12 00:26:43 -07:00
FreeScout
0826102f6e Preserve elements inside [if mso] when purifying thread content 2024-03-12 00:13:27 -07:00
FreeScout
2ff52a0359 Use latest PHP version for linting #3864 from fulldecent/patch-15
Use latest PHP version for linting
2024-03-12 09:54:41 +03:00
William Entriken
8d70999498 Use latest PHP version for linting 2024-03-10 13:25:44 -04:00
FreeScout
f6a4581dad Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2024-03-08 06:49:51 -08:00
FreeScout
2d74977ed3 v1.8.126 2024-03-08 06:49:39 -08:00
FreeScout
8b06724b67 Fix an issue with updating modules 2024-03-08 06:48:51 -08:00
FreeScout
c4755fa920 Disable Undo reply feature for chat conversations in Chat Mode - closes #3856 2024-03-08 06:17:44 -08:00
FreeScout
36f146a71d Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2024-03-08 01:17:16 -08:00
FreeScout
a5515d8520 v1.8.125 2024-03-08 01:16:49 -08:00
FreeScout
33514c6e25 Fixed an issue with opening Closed folder - closes #3858 2024-03-08 01:16:14 -08:00
FreeScout
d8a582aaa5 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2024-03-08 00:23:41 -08:00
FreeScout
a428d33964 v1.8.124 2024-03-08 00:23:25 -08:00
FreeScout
33639a8955 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2024-03-08 00:09:52 -08:00
FreeScout
4021f07aec v1.8.123 2024-03-08 00:09:43 -08:00
FreeScout
67bd456b63 Update Readme 2024-03-07 22:08:26 -08:00
FreeScout
78cc5877a3 Update readme 2024-03-07 10:43:17 -08:00
FreeScout
c4bc505e03 Accessibility of exact date in conversation table - closes #3829 2024-03-07 10:41:43 -08:00
FreeScout
ad372590a9 Allowing updates for third party modules #3851 from Lars-/master
Allowing updates for third party modules
2024-03-07 13:35:03 +03:00
Lars Jansen
2771d526be chore: added comments 2024-03-07 11:24:04 +01:00
Lars Jansen
263ec4d266 fix: code styling 2024-03-07 11:03:46 +01:00
FreeScout
f86876328e Accessibility of exact date in conversation table - closes #3829 2024-03-06 21:55:33 -08:00
Lars Jansen
e47167b17e feat: allow updates for external modules 2024-03-05 20:46:43 +01:00
Lars Jansen
5c32cf69e6 fix: typo 2024-03-05 15:39:31 +01:00
Lars Jansen
fd46017c45 feat: add author to module card if available 2024-03-05 15:39:26 +01:00
FreeScout
95d3cca660 Do not save SMTP username and password in Send Log status message 2024-03-05 06:39:24 -08:00
FreeScout
1fcb5560f0 Fix Passing null to parameter error in Helper::stripTags() - closes #3849 2024-03-05 01:30:31 -08:00
FreeScout
06b3dc27d2 Fix creating conversations by a user seeing only assigned conversations - closes #3843 2024-03-04 05:36:57 -08:00
FreeScout
d2b683368c If user can see only assigned conversations send him notifications only for assigned to him conversations - closes #3843 2024-03-02 21:32:19 -08:00
FreeScout
6b397ccd3f Fix creating conversations by a user seeing only assigned conversations - closes #3843 2024-03-02 21:09:50 -08:00
FreeScout
32006fe8e1 Fix adding notes to Spam conversations 2024-03-02 04:58:52 -08:00
FreeScout
911e157322 Fix showing only assigned to the user tickets - closes #3843 2024-03-02 04:44:23 -08:00
FreeScout
100b4062bf Fix Numeric value out of range error in search on PostgreSQL - closes #3831 2024-02-27 23:04:44 -08:00
FreeScout
b27e2b7a1c Update Czech translation 2024-02-24 06:50:52 -08:00
FreeScout
7085009667 Update Spanish translation 2024-02-24 06:24:34 -08:00
FreeScout
4b6ceab070 Fix word plus colon mistakenly converted to URL in editor - closes #3822 2024-02-24 03:54:38 -08:00
FreeScout
49af472617 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2024-02-23 06:52:13 -08:00
FreeScout
d7aeb0d3b3 v1.8.122 2024-02-23 06:52:03 -08:00
FreeScout
35f8b775f9 Fix an issue with minify.config.php - closes #3818 2024-02-23 06:49:37 -08:00
FreeScout
079df0d6a6 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2024-02-23 03:26:42 -08:00
FreeScout
36442cb3a2 v1.8.121 2024-02-23 03:26:32 -08:00
FreeScout
217cd9183e Add Turkish language 2024-02-20 03:12:36 -08:00
FreeScout
e9c2eaebce Fix showing scaled image in threads - closes #3798 2024-02-20 03:00:48 -08:00
FreeScout
954c790827 Update it.json #3755 from PeopleInside/patch-1
Update it.json
2024-02-20 11:53:55 +03:00
FreeScout
2d2b476688 Remove APP_URL from minify.config.php to avoid CORS issues on custom domains 2024-02-20 00:21:20 -08:00
FreeScout
ab58217cab Improve sorting elements on mobile devices - closes #3800 2024-02-18 22:29:13 -08:00
FreeScout
6f0b05c1f6 Improve searching customers by phone number when creating Phone conversations - closes #3786 2024-02-18 08:54:17 -08:00
FreeScout
58d35098c0 Allow to set session.same_site via SESSION_SAME_SITE env parameter - closes #3023 2024-02-18 05:54:38 -08:00
FreeScout
44e71e58e6 Remove 255 symbols limit from mailbox Alises 2024-02-18 05:36:30 -08:00
FreeScout
0e516bc407 Allow style attribute for tr in HTML purifier - closes #3799 2024-02-18 05:28:06 -08:00
Marco Borla
9fc7b9d755 Update it.json
Fix Soggetto is Oggetto.

Subject = Oggetto
2024-02-02 10:46:40 +01:00
FreeScout
e1ff8fb520 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2024-02-01 17:35:42 -08:00
FreeScout
adf308ac9f v1.8.120 2024-02-01 17:35:31 -08:00
FreeScout
7b6abe858c Fix an issue with multiple emails being sent to customers - closes #3632 2024-01-31 23:17:48 -08:00
FreeScout
6937346e5a Show .parsley-required in red color 2024-01-31 23:10:08 -08:00
FreeScout
fc1a354072 Update Italian translation #3739 from floatingpurr/fix-transalation
✨ Update Italian translation
2024-01-26 19:41:15 +03:00
Andrea Mignone
82464e4129 ✨ Modify Italian translation 2024-01-26 15:29:04 +01:00
FreeScout
1a126ad743 Add parameter to open tracking URL 2024-01-26 06:01:43 -08:00
FreeScout
9558194bf7 Fix redeclaration of let DragDropTouch on Translate page - closes #3717 2024-01-26 00:51:00 -08:00
FreeScout
8aa2f0c018 Improve contrast on on unread notifications 2024-01-24 22:26:11 -08:00
FreeScout
d6de310363 Improve contrast on on unread notifications #3728 from mokkin/patch-1
Improve contrast on on unread notifications
2024-01-25 09:17:59 +03:00
mokkin
1fdddb25ec improve contrast on on unread notifications
fixes https://github.com/freescout-helpdesk/freescout/issues/3693
2024-01-23 17:15:15 +01:00
FreeScout
a78f6f5ce3 Add vendor files to dist 2024-01-23 02:01:04 -08:00
FreeScout
2528727d81 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2024-01-23 02:00:40 -08:00
FreeScout
97bb4dd956 v1.8.119 2024-01-23 02:00:23 -08:00
FreeScout
3c6819c9e7 Fix scrolling conversations list on mobile devices - closes #3724 2024-01-23 01:59:33 -08:00
FreeScout
bb2fc15c07 Fix error in HTMLPurifier/Length.php - closes #3721 2024-01-23 00:45:39 -08:00
FreeScout
fb129df215 Fix Bootstrap submenu after applying DragDropTouch Polyfill - closes #3708 2024-01-22 01:05:29 -08:00
FreeScout
050ccb921e Fix Trailing data error on PostgreSQL - closes #3702 2024-01-20 22:05:39 -08:00
FreeScout
9b9aae4c0e Add vendor files to dist 2024-01-19 23:40:40 -08:00
FreeScout
263569b163 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2024-01-19 23:40:11 -08:00
FreeScout
2702232eea v1.8.118 2024-01-19 23:40:02 -08:00
FreeScout
8f802b4d38 Update install.sh 2024-01-19 23:38:24 -08:00
FreeScout
00b4aabb32 Update Slovak translation #3692 from jozefrebjak/sk-lang-update
Update Slovak translation
2024-01-20 10:31:24 +03:00
Jozef Rebjak
8ea84dca94 lang: update slovak translation 2024-01-16 15:02:22 +01:00
FreeScout
bc45b779ef Fix Use of undefined constant FT_UID in Webklex/laravel-imap - closes #2604 2024-01-15 19:06:51 -08:00
FreeScout
c72286623f Fix Use of undefined constant FT_UID in Webklex/laravel-imap - closes #2604 2024-01-11 21:56:49 -08:00
FreeScout
1d3c97fae5 Do not show Delete button in bulk actions if user does not have corresponding permission - closes #3680 2024-01-11 10:35:19 -08:00
FreeScout
17e22b8b93 Improve HTML purification - closes #3664 2024-01-11 10:24:46 -08:00
FreeScout
e32714ebaf Make html5sortable.js work on mobile devices - closes #3671 2024-01-09 23:33:53 -08:00
FreeScout
acd81548e6 Make summernote compatible with jQuery v3.6.0 - closes #3666 2024-01-08 09:23:07 -08:00
FreeScout
2d71860fa1 Fix counter not aligned in Dashboard - closes #3665 2024-01-08 04:16:19 -08:00
FreeScout
6930924cc1 Update German translation 2024-01-06 21:02:28 -08:00
FreeScout
e5ce253daf Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2024-01-06 04:08:07 -08:00
FreeScout
b9cfe0e300 v1.8.117 2024-01-06 04:07:56 -08:00
FreeScout
af91c8c9e3 Fix Call to undefined method getHashedRelySeparator() 2024-01-06 04:06:14 -08:00
FreeScout
08e50e3643 Localize calendar 2024-01-06 03:55:51 -08:00
FreeScout
885001ec97 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2024-01-05 20:47:49 -08:00
FreeScout
b2dae09cf1 Improve fetching email in Show Original window - closes #3658 2024-01-05 10:08:21 -08:00
FreeScout
c55d94d969 Add alternative reply separation method - closes #3648 2024-01-05 08:35:15 -08:00
FreeScout
7d035f5604 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2024-01-05 08:20:36 -08:00
FreeScout
a5f49080a7 v1.8.116 2024-01-05 08:20:21 -08:00
FreeScout
8a52a85ca3 Replace shell_exec() with Helper::shellExec() 2024-01-05 03:28:30 -08:00
FreeScout
2bfdc99f89 Add alternative reply separation method - closes #3648 2024-01-04 02:03:36 -08:00
FreeScout
e5da621736 Copy attachments when creating a new conversation from some message - closes #3647 2024-01-03 23:57:07 -08:00
FreeScout
359f6c455c Add alternative reply separation method - closes #3648 2024-01-03 22:22:59 -08:00
FreeScout
e929af5c99 Upgrade Bootstrap to v3.4.1 2024-01-03 03:17:36 -08:00
FreeScout
191e61400d Upgrade jQuery to v3.6.0 2024-01-03 03:15:52 -08:00
FreeScout
d311b40faa Allow to filter conversation by Assignee - closes #3641 2024-01-02 03:31:34 -08:00
FreeScout
7c7fc41b2c Add America/Santiago timezone - closes #3639 2023-12-31 07:48:11 -08:00
FreeScout
5ab0f84d41 Refine the Italian translation #3634 from floatingpurr/fix-translation
Refine the Italian translation
2023-12-27 20:44:16 +03:00
Andrea Mignone
5917b190eb Refine and make consistent Italian translation 2023-12-27 18:40:18 +01:00
FreeScout
828c7b814c Show SMTP communication details when testing mail sending - closes #3626 2023-12-27 02:12:06 -08:00
FreeScout
6d3f1bbee8 Show SMTP communication details when testing mail sending - closes #3626 2023-12-27 01:04:03 -08:00
FreeScout
85954ab7cc Add vendor files to dist 2023-12-26 21:49:22 -08:00
FreeScout
822b2f4ae7 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2023-12-26 21:48:38 -08:00
FreeScout
ed6911a8dc v1.8.115 2023-12-26 21:30:05 -08:00
FreeScout
d8cfe15373 Improve linkify algorithm 2023-12-23 01:50:22 -08:00
FreeScout
dc4d6d2671 Add StartTLS encryption option for Fetching Email when Webklex/php-imap is used - closes #3624 2023-12-22 03:27:03 -08:00
FreeScout
eb6b30864f Improve the message shown when IMAP folder can not be found during fetching 2023-12-22 00:25:26 -08:00
FreeScout
1618bb1266 Allow to fetch emails from nested folders - closes #3623 2023-12-21 23:11:39 -08:00
FreeScout
54e5aa28bd Do not set customer last name when it was manually removed - closes #3622 2023-12-21 00:55:22 -08:00
FreeScout
4e72e2ea50 Fix null error in locale_options template - closes #3620 2023-12-20 03:41:08 -08:00
FreeScout
134608625f Fix Packets out of order error - closes #3619 2023-12-20 03:34:55 -08:00
FreeScout
11fbaa6be8 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2023-12-19 21:59:34 -08:00
FreeScout
377dd08c96 v1.8.114 2023-12-19 21:59:03 -08:00
FreeScout
6a9dfe6dd1 Fix email send loop - closes #3607 2023-12-19 21:30:02 -08:00
FreeScout
59da4e4f99 Fix email send loop - closes #3607 2023-12-19 09:37:58 -08:00
FreeScout
6d917f82d7 Add as CC a customer who was in CC and replied to the conversation - #3613 2023-12-16 21:15:02 -08:00
FreeScout
15780467eb Do not strip HTML tags in plain text emails - closes #3614 2023-12-16 04:09:15 -08:00
FreeScout
1a53871517 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2023-12-14 23:58:28 -08:00
FreeScout
ef0af3a494 v1.8.113 2023-12-14 23:58:15 -08:00
FreeScout
ab854befd1 Helper::sanitizeDatepickerDatetime() function 2023-12-14 21:58:12 -08:00
FreeScout
3d522c4f2e Show notice on System Status page is CloudFlare is used 2023-12-13 09:32:18 -08:00
FreeScout
7908da3721 Allow to translate English texts - closes #3599 2023-12-13 06:00:12 -08:00
FreeScout
f9a3354741 Fix user reply to a notification being incorrectly cropped - closes #3580 2023-12-12 09:57:03 -08:00
FreeScout
fbf4d3b355 Improve SendReplyToCustomer background jobs monitoring - closes #3590 2023-12-12 03:43:31 -08:00
FreeScout
f1839bcfbf Fix user reply to a notification being incorrectly cropped - closes #3580 2023-12-11 10:23:17 -08:00
FreeScout
895af2d7ad Make sure thread_id is null for an attachment when linking attachment to a thread 2023-12-11 06:09:38 -08:00
FreeScout
b5510da291 Do not remove double dots from attachment names - closes #3592 2023-12-09 10:40:25 -08:00
FreeScout
3083738d00 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2023-12-08 02:45:34 -08:00
FreeScout
ea84fe82dd v1.8.112 2023-12-08 02:45:08 -08:00
FreeScout
ea8b7507e4 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2023-12-07 23:35:26 -08:00
FreeScout
7dfd170983 Allow .PEM insertion for MySQL connections #3582 from crixle/master
Allow .PEM insertion for MySQL connections
2023-12-06 11:53:58 +03:00
Crixle
06c7e709e6 Allow .PEM insertion for MySQL connections
Required for PaaS DB servers that require SSL.
2023-12-05 17:22:06 -05:00
FreeScout
e0f10670d7 Improve linkify algorithm - closes #3402 2023-12-03 02:27:03 -08:00
FreeScout
b33d2dff4a Fix Call to undefined function shell_exec() on Status page - closes #3570 2023-12-02 21:32:27 -08:00
FreeScout
2f1b9ebe0c conversation.is_user_assignee hook - closes #3568 2023-12-02 04:18:52 -08:00
FreeScout
9b024bda31 Do not change customer when CCed email replies to a conversation - closes #3551 2023-12-02 00:46:47 -08:00
FreeScout
606dcf17c9 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2023-12-01 21:20:17 -08:00
FreeScout
2fb0c29705 v1.8.110 2023-12-01 21:20:07 -08:00
FreeScout
d673a7006b Fix Select None button - closes #3546 2023-11-24 21:11:49 -08:00
FreeScout
fbe3a795df Merge multiple conversations at once - closes #3516 2023-11-23 23:04:51 -08:00
FreeScout
0c614c3f7e Make error message in logs more descriptive when SendReplyToCustomer job can not save outgoing reply to the IMAP folder 2023-11-22 23:52:36 -08:00
FreeScout
ce4bebb8b4 Autosave new conversation draft when choosing recipient - closes #3538 2023-11-22 21:23:15 -08:00
FreeScout
a8b4a581e4 Update Readme 2023-11-21 23:21:39 -08:00
FreeScout
7d5cc80898 Update readme 2023-11-21 23:18:19 -08:00
FreeScout
7fa6003156 Update Portuguese translation 2023-11-21 21:47:51 -08:00
FreeScout
87cdff17b8 Improve linkify algorithm - closes #3402 2023-11-19 23:06:52 -08:00
FreeScout
4fbe3a3340 Allow to select multiple conversations using SHIFT button - closes #1312 2023-11-19 22:02:36 -08:00
FreeScout
c2a8894d9f Dynamically load customer info when creating new conversation - closes #2402 2023-11-18 04:02:19 -08:00
FreeScout
4bb7e224d7 Show customer info when opening new conversation draft - closes #2402 2023-11-18 01:02:00 -08:00
FreeScout
c24efff92f Check pcntl_signal() function on System Status page in the console PHP 2023-11-17 22:28:01 -08:00
FreeScout
9dc73c29ff Set min-height for .container 2023-11-17 22:16:21 -08:00
FreeScout
c2d599b546 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2023-11-17 21:20:09 -08:00
FreeScout
574d1d1d69 v1.8.109 2023-11-17 21:19:57 -08:00
FreeScout
f3ff95cc57 Allow to set SMTP timeout via MAIL_SMTP_TIMEOUT .env parameter 2023-11-17 05:30:47 -08:00
FreeScout
4a2f4fdadc Allow to set SMTP timeout via MAIL_TIMEOUT .env parameter 2023-11-16 21:56:58 -08:00
FreeScout
c2bd39a65f Mark email as read when saving to IMAP Folder with Webklex/PHPIMAP - closes #3533 2023-11-16 10:54:52 -08:00
FreeScout
529b141f1b Fix URLs for attachments with percents - closes #3530 2023-11-15 21:16:46 -08:00
FreeScout
de643ee51b Remove max length limitation from mailbox Aliases field - closes #3525 2023-11-13 22:01:59 -08:00
FreeScout
1dd59bc2b6 Fix displaying SVG images in the uploaded folder on Apache - closes #3522 2023-11-12 22:45:55 -08:00
FreeScout
8b80bd4119 Allow to set table prefix via .env DB_TABLE_PREFIX parameter - closes #3523 2023-11-12 22:23:13 -08:00
FreeScout
0ca0ea0e93 Check pcntl_signal() function on System Status page - closes #3515 2023-11-12 22:18:54 -08:00
FreeScout
4f15b2f273 Allow to retry thread sending from conversation - closes #3517 2023-11-09 05:31:47 -08:00
FreeScout
63e2384907 PHP IMAP: Only explode the tag line if it contains a space - closes #3503 2023-11-08 09:21:41 -08:00
FreeScout
e9b66ff7fd Allow to pass custom message to \Helper::denyAccess() 2023-11-08 09:21:15 -08:00
FreeScout
0eff48267f Fix cicle between two tickets with older newer buttons - closes #3506 2023-11-07 22:04:53 -08:00
FreeScout
4c9f247cf6 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2023-11-06 23:50:12 -08:00
FreeScout
5aa7552232 v1.8.108 2023-11-06 23:49:59 -08:00
FreeScout
0dd1a7308a Fixed saving outgoing replies to the IMAP Sent folder with non-ASCII name 2023-11-06 23:48:51 -08:00
FreeScout
315aeafd24 Fix fetching emails from non-ascii imap folders - closes #3502 2023-11-06 23:33:47 -08:00
FreeScout
74fd2dcf5e Fix Passing null to parameter error in User.php - closes #3501 2023-11-06 05:20:33 -08:00
FreeScout
da66ab2fc6 Add html5sortable.js script 2023-11-04 21:49:58 -07:00
FreeScout
4835fba6dc Fix an issue with phone customers always shown in the search - closes #3499 2023-11-04 05:02:48 -07:00
FreeScout
c28b663f56 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2023-11-03 23:48:43 -07:00
FreeScout
cc2ece53ee v1.8.107 2023-11-03 23:48:35 -07:00
FreeScout
ccc23e6e52 Update install.sh 2023-11-03 23:47:50 -07:00
FreeScout
cee5c16aa1 Adjust installer styles 2023-11-03 23:34:05 -07:00
FreeScout
84db98515d Make install.sh script compatible with Debian 2023-11-03 23:33:48 -07:00
FreeScout
4a24df3395 Fix installing helpdesk without HTTPS via install.sh script 2023-11-03 23:33:10 -07:00
FreeScout
f21054f17f Helper::sqlSanitizeString() function 2023-11-03 03:13:10 -07:00
FreeScout
e12824008f Update general_help_request.md 2023-11-01 10:45:00 -07:00
FreeScout
0c24e04b8c Improve font color sanitizing when purifying thread content - closes #3492 2023-11-01 10:23:33 -07:00
FreeScout
e66aae1209 Set max length for attribtues before saving a user to avoid String data right truncated error on PostgreSQL - closes #3489 2023-11-01 06:07:03 -07:00
FreeScout
e652a1eced Set system timezone by default for users created outside of web interface - closes #3490 2023-10-31 23:20:44 -07:00
FreeScout
bbfbd43105 Fix incorrect redirect when replying to the last active conversation - closes #3486 2023-10-31 07:10:05 -07:00
FreeScout
62bc9643fd .link-grey-blue style 2023-10-31 07:08:24 -07:00
FreeScout
a36bbb2ffa Update Polish translation 2023-10-31 00:31:28 -07:00
FreeScout
c58e2403ca Add dashboard.heading_append hook 2023-10-28 22:07:23 -07:00
FreeScout
926327156d Dynamically expand Logs Monitoring settings in Alerts 2023-10-27 22:19:32 -07:00
FreeScout
45a7f9ae65 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2023-10-27 01:22:55 -07:00
FreeScout
26536b965c v1.8.106 2023-10-27 01:22:47 -07:00
FreeScout
896de29721 Add Max Message Size option to General Settings - closes #3479 2023-10-26 02:01:17 -07:00
FreeScout
fdafd95584 Fix: support overriding of viewable attachments from .env #3481 from InteractiveWebdesign/patch-1
Fix: support overriding of viewable attachments from .env
2023-10-26 10:42:53 +03:00
Interactive-Webdesign
e0f31775e5 fix: support overriding of viewable attachments from .env 2023-10-26 09:38:49 +02:00
FreeScout
67a67aef88 Show All from current mailbox link in conversations - closes #3480 2023-10-25 22:19:41 -07:00
FreeScout
0c28152d43 Fix multiplying attachments when forwarding a conversation 2023-10-24 22:20:46 -07:00
FreeScout
a361b3ba41 Fetch emails into several mailboxes when customer replies - closes #3473 2023-10-24 11:12:56 -07:00
FreeScout
20fe99a820 Fix multiplying attachments when forwarding a conversation 2023-10-23 22:58:40 -07:00
FreeScout
d380d5dd0e Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2023-10-22 23:41:30 -07:00
FreeScout
7df8b05e00 v1.8.105 2023-10-22 23:41:23 -07:00
FreeScout
7708fa86bd Allow to set APP_CLOUDFLARE_IS_USED=true for proper client IP detection when CloudFlare is used - closes #3467 2023-10-22 23:34:01 -07:00
FreeScout
ccb865821d Improve converting URLs into clickable links - closes #3464 2023-10-18 01:41:35 -07:00
FreeScout
7ff10f5510 Allow more attributes when purifying HTML - closes #3463 2023-10-18 00:58:33 -07:00
FreeScout
fce9f83459 Add X-Frame-Options header 2023-10-17 23:20:30 -07:00
FreeScout
74609b0705 Add client_max_body_size to nginx config in install.sh - closes #3453 2023-10-15 00:52:43 -07:00
FreeScout
5efc10ede6 Chat Mode 2023-10-14 06:38:55 -07:00
FreeScout
99bab479e0 Fix an issue when support agent replies to the email notifications and conversation moved to another mailbox - closes #3455 2023-10-12 23:15:16 -07:00
FreeScout
70f11046f1 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2023-10-12 21:54:05 -07:00
FreeScout
6094bddf6a v1.8.104 2023-10-12 21:53:43 -07:00
FreeScout
490d571f3c Add icon to chat conversations tag 2023-10-12 01:50:03 -07:00
FreeScout
18dc66f319 Update install.sh 2023-10-10 00:05:24 -07:00
FreeScout
ba174ed435 Fix apache headers module not found - closes #3450 2023-10-09 22:29:59 -07:00
FreeScout
33723056ec Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2023-10-08 11:11:13 -07:00
FreeScout
5f9c615ac5 v1.8.103 2023-10-08 11:11:04 -07:00
FreeScout
76b3776d79 Fix CSP error on module install - closes #3445 2023-10-08 11:09:51 -07:00
FreeScout
b3f2cddd2d Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2023-10-07 22:02:17 -07:00
FreeScout
d9dac047a0 v1.8.102 2023-10-07 22:01:58 -07:00
FreeScout
a863377597 Enable Content Security Policy 2023-10-07 22:00:57 -07:00
FreeScout
5420cd415b Do not add paperclip to conversations when email contains only embedded images and do not show embedded images as attachments in conversations 2023-10-07 03:53:24 -07:00
FreeScout
1d79d7fe00 Save base64 data images as attachments on fetching 2023-10-05 23:44:42 -07:00
FreeScout
fa6bc246de Add vendor files to dist 2023-10-04 22:20:25 -07:00
FreeScout
c9d5f9f7fa Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2023-10-04 22:18:40 -07:00
FreeScout
b0df2d6898 v1.8.101 2023-10-04 22:18:18 -07:00
FreeScout
a0f4c6b6cd Fix error in PostgreSqlPlatform.php on PHP 8.2 with PostgreSQL 2023-10-04 04:06:19 -07:00
FreeScout
309775cce2 Update test-pgsql.yml 2023-10-04 03:16:47 -07:00
FreeScout
12dcdfbf4a Update test-pgsql.yml 2023-10-04 03:05:17 -07:00
FreeScout
1fc7042545 Test Application (with PostgreSQL) 2023-10-04 03:01:23 -07:00
FreeScout
9089aba931 Update email.reply_to_customer.subject hook 2023-10-02 04:48:35 -07:00
FreeScout
b12c6102c6 Update test.yml 2023-10-02 01:04:04 -07:00
FreeScout
5548148150 Update user getAuthToken() 2023-10-01 23:48:46 -07:00
FreeScout
aff6e4acb0 CSP compatibility 2023-09-30 10:33:41 -07:00
FreeScout
1e88014f38 Configure CSP 2023-09-30 07:38:06 -07:00
FreeScout
5be74723d5 Configure CSP 2023-09-30 03:04:11 -07:00
FreeScout
f0ee5ee2ae Configure CSP 2023-09-29 09:54:13 -07:00
FreeScout
1daa34f604 Improve uploads folder security - closes #3428 2023-09-27 10:16:34 -07:00
FreeScout
258b774098 Run post update after modules in update.sh #3425 from webmaster777/post-update-after-modules
Run post update after modules in update.sh
2023-09-27 19:52:23 +03:00
FreeScout
1b6f9e4f09 Improve uploads folder security - closes #3428 2023-09-27 07:15:24 -07:00
Meindert-Jan Kroese
2598263cab run post_update.sh at the very end of updating (incl modules) 2023-09-26 14:31:04 +02:00
FreeScout
98314a9a26 Update German translation 2023-09-26 00:54:27 -07:00
FreeScout
20074845de Remove phpstan 2023-09-23 23:04:58 -07:00
FreeScout
d0b1c816eb Update lint-php.yml 2023-09-23 22:54:56 -07:00
FreeScout
5f1a59f7b3 Update phpcs and phpstan 2023-09-23 22:18:14 -07:00
FreeScout
c7644e214d Update analyse-php.yml 2023-09-23 11:40:59 -07:00
FreeScout
b6d026a2b1 Update analyse-php.yml 2023-09-23 11:37:02 -07:00
FreeScout
23c6cb33a3 Update analyse-php.yml 2023-09-23 11:29:44 -07:00
FreeScout
e68e8ef165 analyse-php.yml 2023-09-23 11:14:31 -07:00
FreeScout
912b805777 lint-js.yml 2023-09-23 11:02:51 -07:00
FreeScout
cacdf09649 lint-php.yml 2023-09-23 10:59:56 -07:00
FreeScout
b32320d59c Fix db seed error on PHP 8.2 2023-09-23 08:48:25 -07:00
FreeScout
0e2cc687d4 Rename mysql_testing DB to testing 2023-09-23 08:26:31 -07:00
FreeScout
1bc82611e9 Commit tests/Feature/ folder 2023-09-23 08:23:45 -07:00
FreeScout
6bb27b4e6f Update test.yml 2023-09-23 08:15:20 -07:00
FreeScout
3a6d5b3e34 Configure mysql_testing in database.php 2023-09-23 08:09:42 -07:00
FreeScout
33ccc85fbf Update test.yml 2023-09-23 07:47:40 -07:00
FreeScout
a21a7dbeda Update test.yml 2023-09-23 07:36:17 -07:00
FreeScout
718dc2c691 Update test.yml 2023-09-23 07:34:25 -07:00
FreeScout
7ca5a92e9f Update test.yml 2023-09-23 07:30:24 -07:00
FreeScout
7a9ecfe329 Update test.yml 2023-09-23 07:25:00 -07:00
FreeScout
ffd299f369 Update phpunit/phpunit to 9.5.28 2023-09-23 06:32:51 -07:00
FreeScout
fbf2f1f063 Add vendor files to dist 2023-09-23 03:18:04 -07:00
FreeScout
66c1ca83d9 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2023-09-23 03:10:53 -07:00
FreeScout
a7e542b7b2 v1.8.100 2023-09-23 03:10:22 -07:00
FreeScout
90d73053f1 Fix discarding new phone conversation draft - closes #3407 2023-09-22 23:56:37 -07:00
FreeScout
900f259620 Remove yarn.lock 2023-09-22 23:46:20 -07:00
FreeScout
b7367f89e2 Merge pull request #3408 from baldugus/master
Add filter `fetch_emails.mailbox_to_save_message`
2023-09-23 09:43:10 +03:00
FreeScout
35ba1d2ca5 Improve accessibility from philippkraft/master
Improve accessibility - addendum to issue #1150
2023-09-23 09:40:22 +03:00
FreeScout
cf072514a4 Fix OS Command Injection issue in laravel/framework 2023-09-22 23:24:08 -07:00
FreeScout
6133547617 Fix Possibility for Denial of Service by overwriting PHP files with language exports in barryvdh/laravel-translation-manager 2023-09-22 23:15:15 -07:00
FreeScout
d62bf49eb9 Fix CVE-2023-35169 issue in webklex/laravel-imap 2023-09-22 23:09:29 -07:00
FreeScout
7b32dde0cf Fix CVE-2023-35169 issue in webklex/php-imap 2023-09-22 23:04:14 -07:00
FreeScout
8c7e53aef8 Fix the issue causing long fetching process to be killed before it finishes - closes #3417 2023-09-22 21:59:00 -07:00
FreeScout
2d9e9091b4 Fix the issue causing long fetching process to be killed before it finishes - closes #3417 2023-09-22 10:48:21 -07:00
FreeScout
8363650304 Fix Cookie serialization vulnerability in laravel/framework 2023-09-22 07:06:43 -07:00
FreeScout
21d86327b9 Fix Guard bypass in Eloquent models in laravel/framework 2023-09-22 04:52:43 -07:00
FreeScout
1e871813f8 Fix CVE-2021-43808 issue in laravel/framework 2023-09-22 04:31:41 -07:00
FreeScout
822fb85f1c Fix RCE vulnerability in cookie session driver in laravel/framework 2023-09-22 02:07:50 -07:00
Philipp Kraft
ae0e931588 Merge branch 'freescout-helpdesk:master' into master 2023-09-22 10:29:24 +02:00
FreeScout
6bb91df75f Fix CVE-2019-18887 issue in symfony/http-kernel 2023-09-22 01:10:19 -07:00
FreeScout
9c1c180619 Fix CVE-2022-24894 issue in symfony/http-kernel 2023-09-22 01:01:59 -07:00
Gustavo Balduino
4b27fe0f82 Add filter fetch_emails.mailbox_to_save_message 2023-09-20 16:12:51 -03:00
FreeScout
aab5869dc9 Run test workflow on push to dist branch 2023-09-17 02:54:02 -07:00
FreeScout
c6b50b2ccf Fix CVE-2019-18888 issue in symfony/http-foundation 2023-09-17 02:49:35 -07:00
FreeScout
ba8296ef50 Fix CVE-2019-10913 issue in symfony/http-foundation 2023-09-17 01:44:45 -07:00
FreeScout
5b5df2e51e Upgrade league/flysystem to 1.1.4 2023-09-17 01:34:37 -07:00
FreeScout
73c039147e Upgrade guzzlehttp/guzzle to 6.5.8 2023-09-17 01:20:35 -07:00
FreeScout
51ef78c2af Upgrade erusev/parsedown to 1.7.2 2023-09-17 00:33:09 -07:00
FreeScout
42423bdf61 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2023-09-15 07:52:40 -07:00
FreeScout
35e2769ba3 Add freescout:module-update command and --yes flag to tools/update.sh - closes #3384 2023-09-15 07:52:14 -07:00
FreeScout
dc3e56d44b Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2023-09-15 07:50:28 -07:00
FreeScout
4d59b4b463 v1.8.99 2023-09-15 07:50:00 -07:00
FreeScout
ae9f1f8b0d Update install script path in continuous integration test 2023-09-15 07:43:29 -07:00
FreeScout
e30b9ed175 Create continuous integration test #3395 from fulldecent/patch-13
Create continuous integration test
2023-09-15 17:41:19 +03:00
FreeScout
d06dbcb99d Add freescout:module-update command and --yes flag to tools/update.sh - closes #3384 2023-09-15 07:37:53 -07:00
FreeScout
70af65c51f Add tools/install.sh 2023-09-14 22:10:37 -07:00
William Entriken
d288f3319a Create continuous integration test 2023-09-14 21:43:04 -04:00
FreeScout
96b1ee7b66 Properly process situation when application can not parse mail date header - closes #3394 2023-09-14 05:51:15 -07:00
FreeScout
89b37e778a Add freescout:module-update command and --yes flag to tools/update.sh - closes #3384 2023-09-14 00:13:51 -07:00
FreeScout
a72b859f05 freescout:module-update command - closes #3384 2023-09-13 04:49:02 -07:00
FreeScout
9d1c463247 Update yarn.lock 2023-09-13 04:25:43 -07:00
FreeScout
62c7bed6bb Allow to update all modules at once 2023-09-13 01:43:56 -07:00
FreeScout
3338ea6510 App\Module::updateModule() function 2023-09-13 00:46:49 -07:00
FreeScout
b6c015e3da Remove Mail Date and Time option from Mail Settings - closes #3380 2023-09-11 22:23:22 -07:00
FreeScout
547b05dcae Update package.json 2023-09-11 22:17:20 -07:00
FreeScout
ee7a2e692b Improve attachment names sanitizing - closes #3377 2023-09-11 22:15:10 -07:00
FreeScout
d52161b44b Improve attachment names sanitizing - closes #3377 2023-09-11 10:15:55 -07:00
FreeScout
9b5bf06a29 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2023-09-11 09:25:25 -07:00
FreeScout
189942c45a v1.8.98 2023-09-11 09:24:40 -07:00
FreeScout
8c68731283 Skip module symlink check and creation on open_basedir restriction in effect error - closes #3375 2023-09-11 06:54:48 -07:00
FreeScout
ec24787b9b Skip module symlink check and creation on open_basedir restriction in effect error - closes #3375 2023-09-11 06:42:01 -07:00
FreeScout
c220b8be8e Set default CURLOPT_CONNECTTIMEOUT value from config 2023-09-09 06:47:20 -07:00
FreeScout
550d8cc457 Fix No query results for model error on Status page - closes #3366 2023-09-09 03:57:27 -07:00
FreeScout
2de52008a2 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2023-09-08 10:32:38 -07:00
FreeScout
4354742d3f v1.8.97 2023-09-08 10:32:12 -07:00
FreeScout
5e1bf39839 Fix error on System Status page - closes #3368 2023-09-08 09:50:24 -07:00
FreeScout
74ab043a30 Automatically fix module symlink when module Public folder is a broken symlink 2023-09-08 06:22:00 -07:00
FreeScout
2abe265046 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2023-09-08 04:37:02 -07:00
FreeScout
d4f3cd4e6c v1.8.96 2023-09-08 04:36:27 -07:00
FreeScout
0c5a50c34e Remove trailing slash from host name when generating URLs 2023-09-08 01:28:51 -07:00
Philipp Kraft
dfab29bbbc Aria markup for thread options, addendum to #1150
Screen readers do not find the thread-options dropdown, because correct aria markup is missing. Added role, aria-haspopup, and aria-expanded attributes to the dropdown toggle caret
2023-09-08 09:36:57 +02:00
FreeScout
4bbbab7fa1 Automatically check invalid or missing modules symlinks on Modules and System Status pages 2023-09-07 00:12:34 -07:00
FreeScout
4c0b5d1d58 Automatically check invalid or missing modules symlinks on Modules and System Status pages 2023-09-06 23:38:00 -07:00
FreeScout
9f99e784f4 Move star mark when merging conversations - closes #3351 2023-09-06 03:28:55 -07:00
FreeScout
8c673d5242 Do not insert new line in the editor when pasting content - closes #3319 2023-09-05 23:27:01 -07:00
FreeScout
6852d7e14d Update Spanish translation #3343 from Pablohn26/patch-3
Update Spanish translation
2023-09-05 17:31:34 +03:00
FreeScout
204808032e Show SMTP 5xx errors immediately in the conversation - closes #3339 2023-09-05 07:30:19 -07:00
Pablo Hinojosa
8952cdb813 Update es.json
Improve Spanish translation. 
* Adding `¿` and `¡` marks
* Fixing some words without accent mark
2023-09-05 16:17:30 +02:00
FreeScout
bab3eb1e9f Show SMTP Queue ID in Outgoing Emails for sent emails - closes #3330 2023-09-05 04:44:34 -07:00
FreeScout
900bdfb199 Cancel SendReplyToCustomer background jobs when undoing replies - closes #3300 2023-09-05 00:33:52 -07:00
FreeScout
097f404d94 Fix Allow to reply from aliases option being reset when mailbox settings saved by non-admin - closes #3337 2023-09-04 23:30:50 -07:00
FreeScout
f796523e3b Set CURLOPT_SSL_VERIFYPEER option to false for curl and Guzzle requests - closes #3293 2023-09-04 23:24:51 -07:00
FreeScout
44477d7b1a Check symlink function on Status page - closes #3335 2023-09-03 23:49:38 -07:00
FreeScout
f3592d1d60 Fix errors on Status page - closes #3334 2023-09-03 23:35:29 -07:00
FreeScout
b935e20d63 Fix errors on Status page - closes #3334 2023-09-03 23:34:34 -07:00
FreeScout
0e4c8415a1 CustomerChannel::getChannels() function 2023-09-03 08:31:02 -07:00
FreeScout
b342f14bf9 Show DB info and missing migrations on Status page - closes #3326 2023-09-03 03:54:07 -07:00
FreeScout
b8489059f3 Show link to conversation and logs in SendReplyToCustomer jobs on System page - closes #3331 2023-09-03 00:45:01 -07:00
FreeScout
ebfd86bcb9 Show link to conversation and logs in SendReplyToCustomer jobs on System page - closes #3331 2023-09-03 00:39:29 -07:00
FreeScout
1e37ca57bc Store for the future an attempt to get SMTP Queue ID - closes #3330 2023-09-02 23:22:36 -07:00
FreeScout
eabd501dee Update readme 2023-09-02 00:04:35 -07:00
FreeScout
252de7a589 Fix variable name in saveUserThread() on fetching - closes #3325 2023-09-01 06:53:22 -07:00
FreeScout
9be2c1df5a Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2023-09-01 03:11:57 -07:00
FreeScout
a35c78d796 v1.8.95 2023-09-01 03:11:03 -07:00
FreeScout
43a8a759af Add GET page parameter when navigating over conversations - closes #3323 2023-09-01 00:50:37 -07:00
FreeScout
92abebaffa Show correct 12 or 24 hour format in date pickers - closes #3322 2023-08-31 23:53:57 -07:00
FreeScout
3b9b149555 Fix Invalid text representation error on PostgreSQL when saving mailbox permissions - closes #3315 2023-08-30 00:29:27 -07:00
FreeScout
8b3433b4ca Improve mail subject decoding - closes #3312 2023-08-28 23:29:54 -07:00
FreeScout
b02e034621 Fix migration creating customer_channel table - closes #3309 2023-08-28 22:48:59 -07:00
FreeScout
196e484448 Add Mail Date and Time option allowing to use date from mail headers when fetching emails - closes #3297 2023-08-28 00:15:59 -07:00
FreeScout
0cd3952d52 Repalce with empty string non-replaced vars in mailbox custom from name - closes #1031 2023-08-27 07:33:48 -07:00
FreeScout
3640508db9 Fix summernote adding new lines when pasting HTML - closes #3264 2023-08-26 09:17:00 -07:00
FreeScout
e75dcaf282 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2023-08-25 21:58:56 -07:00
FreeScout
a7b6b94612 v1.8.94 2023-08-25 21:58:19 -07:00
FreeScout
6ea0ea4619 Remove old records from notifications table - closes #3294 2023-08-23 10:44:58 -07:00
FreeScout
0c71e9cf5f Fix module translation file never imported under certain condition #3289 from totake0224/master
Fix module translation file never imported under certain condition
2023-08-22 09:48:03 +03:00
totake0224
348fbb237b set $locale before preg_match.because if former foreach ended continue condition. module translation file would be never importerd. 2023-08-22 15:35:15 +09:00
FreeScout
e2808ced1a Add exclude_channel parameter to customer->findCustomersBySocialProfile() 2023-08-21 06:11:52 -07:00
FreeScout
30b481597b Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2023-08-20 23:29:39 -07:00
FreeScout
c31685b062 v1.8.93 2023-08-20 23:29:29 -07:00
FreeScout
8dbe489d60 Fix Unassigned and Assigned folders - closes #3287 2023-08-20 23:28:42 -07:00
FreeScout
b11ffe9afa Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2023-08-20 22:33:03 -07:00
FreeScout
789da461f9 v1.8.92 2023-08-20 22:32:30 -07:00
FreeScout
fb3d1752dc Add an option allowing to restrict users to see only tickets assigned to themselves - closes #701 2023-08-20 03:29:46 -07:00
FreeScout
5219afb13f Add Chinese Simplified language 2023-08-20 00:59:58 -07:00
FreeScout
71e0888c65 Customer findCustomersBySocialProfile() function 2023-08-20 00:03:47 -07:00
FreeScout
e5456c8ca3 Functions for working with customer_channel table 2023-08-19 23:08:03 -07:00
FreeScout
9987ba2fbb In the chat conversation reply area show all buttons after codeview 2023-08-19 23:06:15 -07:00
FreeScout
8ea51c3e41 Update Danish translation 2023-08-19 09:38:24 -07:00
FreeScout
fbd934d750 Store customer to channel mapping in customer_channel table 2023-08-19 08:15:47 -07:00
FreeScout
63d06ee32a Show an error message to support agent if an emial has not been sent after 1 hour - closes #3268 2023-08-18 23:52:54 -07:00
FreeScout
561f1deb07 Add vendor files to dist 2023-08-18 00:04:44 -07:00
FreeScout
584d84475a Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2023-08-18 00:02:31 -07:00
FreeScout
c9e56723fb v1.8.91 2023-08-18 00:01:48 -07:00
FreeScout
b5b6e42c2d Allow to send by email to the customer threads in a phone conversation - closes #3270 2023-08-17 01:26:11 -07:00
FreeScout
14e56cd04f Fix Passing null to parameter in HTMLPurifier/Lexer.php - closes #3277 2023-08-16 22:34:47 -07:00
FreeScout
ecfbdd29a7 Speed up Eventy event firing 2023-08-14 23:30:37 -07:00
FreeScout
321a87c407 Speed up Eventy event firing #3263 from OttoWinter/optimize-eventy
Speed up Eventy event firing
2023-08-15 09:14:11 +03:00
Otto Winter
4403fa2238 Add missed change 2023-08-14 21:55:40 +02:00
Otto Winter
f2623f0253 Optimize eventy 2023-08-14 20:58:42 +02:00
FreeScout
36f16d9f9b Fix routes var in ModuleLaroute.php 2023-08-14 06:01:02 -07:00
FreeScout
097d5e3f98 Condense reply toolbar 2023-08-14 01:47:30 -07:00
FreeScout
87267638f7 Update Czech translation 2023-08-13 06:22:31 -07:00
FreeScout
6bcc9b0cc5 Update Polish translation 2023-08-13 03:37:42 -07:00
FreeScout
40fc845461 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2023-08-11 22:03:54 -07:00
FreeScout
18017c73ce v1.8.90 2023-08-11 22:03:21 -07:00
FreeScout
9e69ae6ec0 Allow to use vars in mailbox Custom From Name - closes #1031 2023-08-11 07:28:43 -07:00
FreeScout
0cb4bbcfe2 Improve subject and attachment name decoding - closes #3248 2023-08-08 22:20:00 -07:00
FreeScout
6a2f5657a5 Add app.curl_user_agent parameter 2023-08-06 03:25:30 -07:00
FreeScout
624d84be41 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2023-08-05 23:10:33 -07:00
FreeScout
8045da1f96 v1.8.89 2023-08-05 23:10:05 -07:00
FreeScout
482f428a17 Fix preg_match() Passing null to parameter in Helper.php - closes #3239 2023-08-05 23:08:59 -07:00
FreeScout
663908d17e Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2023-08-05 11:07:03 -07:00
FreeScout
3c61ff6895 v1.8.88 2023-08-05 11:06:26 -07:00
FreeScout
be504fc15c Fix shell_exec() error on Status page - closes #3239 2023-08-05 11:04:58 -07:00
FreeScout
60c2bd4808 Take into account not only created_at but also thread ID when sorting threads - closes #3240 2023-08-05 04:05:16 -07:00
FreeScout
03c584a5c3 Fix an issue with the customer reply connected to the wrong conversation - closes #3231 2023-08-04 06:47:25 -07:00
FreeScout
3570c48420 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2023-08-03 23:22:23 -07:00
FreeScout
aac5df9748 v1.8.87 2023-08-03 23:21:49 -07:00
FreeScout
884fa08355 Save .ICS attachments having no name as calendar.ics - closes #1412 2023-07-31 22:08:37 -07:00
FreeScout
59dbc520b9 Update Italian translation 2023-07-31 21:46:18 -07:00
FreeScout
8075bf14ec Save CC and BCC when saving outgoing email to the Sent IMAP folder - closes #3228 2023-07-31 04:26:16 -07:00
FreeScout
8e0d993596 Save CC and BCC when saving outgoing email to the Sent IMAP folder - closes #3228 2023-07-31 04:23:53 -07:00
FreeScout
6c87713751 Improve subject decoding - closes #3196 2023-07-18 06:55:38 -07:00
FreeScout
43ecd35402 Improve subject decoding 2023-07-18 06:39:49 -07:00
FreeScout
424c89e2c8 Allow to view .json, .diff and .patch attachments - closes #3192 2023-07-17 23:12:12 -07:00
FreeScout
744ccef561 Check pcntl extension in the console version of PHP on Status page - closes #3190 2023-07-17 22:48:18 -07:00
FreeScout
7f877ee636 Make sure that conversation subject has String type when fetching emails 2023-07-17 10:31:37 -07:00
FreeScout
542d6dc061 Remove FUNDING.yml 2023-07-17 09:56:43 -07:00
FreeScout
bd1bdc03c2 Make sure that conversation subject has String type when fetching emails 2023-07-17 06:22:12 -07:00
FreeScout
b0be55ef2e Update Readme 2023-07-17 03:09:16 -07:00
FreeScout
6f332d8c2d Update readme 2023-07-17 03:06:05 -07:00
FreeScout
cf2f488aaf Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2023-07-17 01:40:15 -07:00
FreeScout
8c91df4319 v1.8.86 2023-07-17 01:39:12 -07:00
FreeScout
807eba8d98 Do not delete the message when another user discards a draft of the reply which just has been sent - closes #2873 2023-07-17 01:04:22 -07:00
FreeScout
71fb3b1313 Allow to set trusted proxies via .env file APP_TRUSTED_PROXIES parameter - closes #1994 2023-07-17 00:12:00 -07:00
FreeScout
2b9f56b456 Helper::getClientIp() function 2023-07-15 23:20:47 -07:00
FreeScout
9266e5c207 Allow to show custom message on 403 error page 2023-07-15 23:02:56 -07:00
FreeScout
ffe2adc4c2 Login hooks 2023-07-15 06:11:45 -07:00
FreeScout
9b6fa32db5 Fix References header in emails to customers - closes #3175 2023-07-14 10:24:07 -07:00
FreeScout
fdea5e302f Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2023-07-14 06:40:55 -07:00
FreeScout
4fbd276fff v1.8.85 2023-07-14 06:40:19 -07:00
FreeScout
90536704a3 Improve iso-2022-jp subjects and attachment names decoding - closes #3185 2023-07-14 04:28:10 -07:00
FreeScout
f8787c67df Update \MailHelper::decodeSubject() 2023-07-14 03:27:06 -07:00
FreeScout
2cea3a6585 Do not create empty conversation when same email is being fetched at the same time - #3186 2023-07-14 03:25:08 -07:00
FreeScout
61edffd0b6 Fix Attempt to read property id on null in reply_fancy - closes #3182 2023-07-13 22:57:42 -07:00
FreeScout
6494ca8c26 List all Message-IDs in References header in replies to customers - closes #3175 2023-07-12 11:05:30 -07:00
FreeScout
480448ff79 Improve suject decoding - closes #3177 2023-07-12 07:18:34 -07:00
FreeScout
8139f91db2 Update Russian translation 2023-07-12 01:21:42 -07:00
FreeScout
3d7f424b28 Limit the number of displayed non writable cache files on Status page - closes #3165 2023-07-09 22:48:33 -07:00
FreeScout
9e514447e6 Merge pull request #3172 from sulemaanhamza/master
Fixed undefined variable bug in the FollowerObserver
2023-07-08 12:15:22 +03:00
suleman
06ed4c8da7 Fixed undefined variable bug in FollowerObserver 2023-07-08 13:47:59 +05:00
FreeScout
072c3f8be9 Add vendor files to dist 2023-07-07 00:28:42 -07:00
FreeScout
37b158e99b Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2023-07-07 00:26:34 -07:00
FreeScout
a327c0b321 v1.8.84 2023-07-07 00:25:55 -07:00
FreeScout
d57dc285df Improve subject decoding - closes #3167 2023-07-06 23:45:46 -07:00
FreeScout
e29cfc7b88 Do not encode HTML entities in outoging plain text emails - closes #3159 2023-07-06 02:56:11 -07:00
FreeScout
08c7a4033e Update German translation 2023-07-06 02:36:32 -07:00
FreeScout
b9dd2b7cbc Fix GuzzleHttp\Psr7\Uri::withScheme() error on PHP 7.1 - closes #3158 2023-07-05 00:20:32 -07:00
FreeScout
e45eccd64c Do not try to resend email notifications on received bounce emails - closes #3156 2023-07-04 23:25:57 -07:00
FreeScout
c93411e330 Update message shown when no activations left for the license key 2023-07-04 23:18:55 -07:00
FreeScout
550e2d30cc Fix PHP version check in tools.php - closes #3162 2023-07-04 22:50:19 -07:00
FreeScout
48e4cfadfd Check pcntl extension status in System to avoid Undefined constant SIGKILL error 2023-07-04 00:57:25 -07:00
FreeScout
c43e41c076 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2023-06-30 20:37:11 -07:00
FreeScout
45af81513c v1.8.83 2023-06-30 20:36:24 -07:00
FreeScout
5244b5c8f0 Fix retrieveing attachments - closes #3154 2023-06-30 20:35:10 -07:00
FreeScout
ff36b2fef1 Add vendor files to dist 2023-06-30 00:40:52 -07:00
FreeScout
b27997db2c Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2023-06-30 00:39:35 -07:00
FreeScout
b0b9a621a1 v1.8.82 2023-06-30 00:38:47 -07:00
FreeScout
f851e70d28 Make using imap_utf8_to_mutf7() function optional in Webklex/laravel-imap library - closes #3152 2023-06-30 00:36:05 -07:00
FreeScout
09a5d1520d email.reply_to_customer.threads hook 2023-06-29 00:48:39 -07:00
FreeScout
7ba097e34c Update NL translation 2023-06-28 23:07:47 -07:00
FreeScout
2141e1f4da Preserve new lines when replacing vars in the signature - closes #3146 2023-06-28 07:20:12 -07:00
FreeScout
9c0fd7b027 Preserve new lines when replacing vars in the signature - closes #3146 2023-06-28 07:10:14 -07:00
FreeScout
fa9289293b Improve subject decoding 2023-06-27 23:36:16 -07:00
FreeScout
3092424315 Fixed saving data into Timestamp field on PostgreSQL 2023-06-27 01:14:05 -07:00
FreeScout
2abe327e90 Fix Undefined property: Webklex\IMAP\Attachment::config - closes #3142 2023-06-26 23:24:11 -07:00
FreeScout
47ebc2f694 Improve decoding strings in Webklex/laravel-imap library 2023-06-26 11:33:57 -07:00
FreeScout
9312036f69 Improve decoding strings in Webklex/laravel-imap library 2023-06-26 07:10:19 -07:00
FreeScout
5732b8456f Update readme 2023-06-25 06:07:21 -07:00
FreeScout
7fd01b7e40 Show proper error when module license key is invalid 2023-06-23 22:21:12 -07:00
FreeScout
fc72dfc870 Show proper error when module license key is invalid 2023-06-23 12:10:04 -07:00
FreeScout
24dc14c44a Add vendor files to dist 2023-06-22 00:14:37 -07:00
FreeScout
1e291d4180 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2023-06-22 00:11:31 -07:00
FreeScout
1af3310e0b v1.8.81 2023-06-22 00:10:52 -07:00
FreeScout
e90c1838f1 Change iso-2022-jp encoding to iso-2022-jp-ms when decoding data in Webklex/php-imap library - closes #3122 2023-06-22 00:05:10 -07:00
FreeScout
88932bfc67 Fix decoding sender name in iso-2022-jp encoding - closes #3089 2023-06-19 06:47:56 -07:00
FreeScout
17205d39d2 Improve decoding attachment names with Webklex/PHP-IMAP library - closes #2753 2023-06-19 06:25:08 -07:00
FreeScout
9512104536 Improve decoding attachment names with Webklex/PHP-IMAP library - closes #2753 2023-06-19 05:06:47 -07:00
FreeScout
4eabdc489f Improve body encoding conversion on fetchign - closes #3089 2023-06-19 02:13:21 -07:00
FreeScout
fd9865b368 Revert formatEmailList() update - closes #3118 2023-06-19 01:49:45 -07:00
FreeScout
c5a8dca36f Prevent Attempt to read property mail on fetching - closes #3118 2023-06-19 00:33:40 -07:00
FreeScout
49d0a2aaf0 Improve subject decoding - closes #3089 2023-06-15 05:40:47 -07:00
FreeScout
b5b941bf73 Do not use @unknown email from Reply-To on fetching - closes #3101 2023-06-14 22:34:15 -07:00
FreeScout
6cb39bd6ee Improve email validation: do not allow @unknown emails - closes #3101 2023-06-13 23:18:26 -07:00
FreeScout
b6b1292589 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2023-06-05 10:32:38 -07:00
FreeScout
2d4e4e71ab v1.8.80 2023-06-05 10:32:08 -07:00
FreeScout
8194d416c6 Fix an issue with HTTPS redirect - closes #3075, #3070 2023-06-05 10:30:13 -07:00
1204 changed files with 92916 additions and 66836 deletions

View File

@@ -12,8 +12,9 @@ APP_URL=https://example.com
# If you are using HTTPS, feel free to uncomment this line to improve security
#SESSION_SECURE_COOKIE=true
# Enter your proxy address here if freescout.net is not available from your server
# (access to freescout.net is required to obtain official modules)
# Enter your proxy address here. It will be used to connect to all external URLs via CURL.
# You may need to do specify proxy for example if api.freescout.net is not available from your server
# (access to api.freescout.net is required to obtain official modules).
#APP_PROXY=
# Custom headers to add to all outgoing emails.
@@ -26,6 +27,16 @@ APP_URL=https://example.com
# Comment it to use default timezone from php.ini
#APP_TIMEZONE=Europe/London
# Comma separated list of trusted proxies for proper IP detection in FreeScout.
# To trust all proxies that connect to your server use single asterisk: *
# To trust ALL proxies, including those that are in a chain of forwarding use double asterisk: **
#APP_TRUSTED_PROXIES=192.168.1.1,192.168.1.2,192.168.1.3
# Set this parameter when receiving "Domain or IP address is not allowed".
# https://github.com/freescout-help-desk/freescout/wiki/Installation-Guide#-domain-or-ip-address-is-not-allowed
# Example: "example.org,12.34.56.78"
#APP_REMOTE_HOST_WHITE_LIST=""
DB_CONNECTION=mysql
DB_HOST=localhost
DB_PORT=3306

3
.github/FUNDING.yml vendored
View File

@@ -1,3 +0,0 @@
# These are supported funding model platforms
patreon: FreeScout

1
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1 @@
blank_issues_enabled: false

View File

@@ -3,18 +3,23 @@ name: General Help Request
about: Create a general help request
---
<!--
* If you have some error the first thing you should do is to check "Manage » Logs » App Logs"! If there are any errors there - make sure to provide them in the issue.
* Have you read FAQ? https://github.com/freescout-helpdesk/freescout/wiki/FAQ
* If you have "Whoops something went wrong" error see: https://github.com/freescout-helpdesk/freescout/wiki/Installation-Guide#11-troubleshooting
* Before creating an issue try to disable all non-official modules and remove all custom code from your FreeScout and see if the issue still persists.
* Feature requests for modules should be posted here: https://freescout.net/request-feature/
* Docker questions should go here: https://github.com/tiredofit/docker-freescout/issues
* Create separate issues for different topics - no need to mix all your problems into one issue.
* Questions on issues with payments for modules should be sent by email: https://freescout.net/contact-us/
* Don't forget that you are not the project manager. Try to be humble, polite, friendly and positive. Otherwise your account may be banned.
* Mentioning how much you've spent on modules is the way to be banned.
* Don't forget that you are not the project manager. Try to be humble, polite, friendly and positive. Otherwise your account may be banned (and it can't be undone).
* Mentioning how much you've spent on modules is also the way to be banned.
Still here? Well clean this out and go ahead :)
-->
PHP version:
FreeScout version:
FreeScout version:
Database: MySQL / PostgreSQL
Are you using CloudFlare: Yes / No
Are you using non-official modules: Yes / No

View File

@@ -1,3 +1,5 @@
Keep in mind that pull requests should be sent to the `master` branch! See https://github.com/freescout-helpdesk/freescout/wiki/Development-Guide#github-workflow
Pull requests should be sent to the `master` branch: https://github.com/freescout-helpdesk/freescout/wiki/Development-Guide#github-workflow
Also one commit is preferable to several commits - it's easier to read and analize single commit.
Now you can delete this text and type the description of your pull request...

23
.github/workflows/phpcs.yml vendored Normal file
View File

@@ -0,0 +1,23 @@
name: PHP Code Sniffer
on:
push:
branches:
- master
workflow_dispatch:
jobs:
build:
name: Lint PHP
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: 8.3
tools: phpcs
- name: Run check
run: phpcs

60
.github/workflows/test-pgsql.yml vendored Normal file
View File

@@ -0,0 +1,60 @@
name: Test App (PostgreSQL)
on:
push:
branches:
- master
workflow_dispatch:
jobs:
test:
name: Test App (PostgreSQL)
runs-on: ubuntu-latest
env:
DB_CONNECTION: testing_pgsql
services:
postgres:
image: postgres:latest
env:
POSTGRES_USER: freescout-test
POSTGRES_PASSWORD: freescout-test
POSTGRES_DB: freescout-test
ports:
- 5432:5432
# Set health checks to wait until postgres has started
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
strategy:
matrix:
php: ['7.4', '8.0', '8.1', '8.2', '8.3', '8.4', '8.5']
steps:
- uses: actions/checkout@v3
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: pgsql, mbstring, xml, imap, zip, gd, curl, intl, json
- name: Install composer dependencies
run: composer install --ignore-platform-reqs --no-interaction
- name: Migrate and seed the database
run: |
php${{ matrix.php }} artisan migrate --force -n --database=testing_pgsql
php${{ matrix.php }} artisan db:seed --force -n --database=testing_pgsql
env:
DB_PORT: ${{ job.services.postgres.ports[5432] }}
- name: Run PHP tests
if: ${{ matrix.php < 8.4 }}
run: php${{ matrix.php }} ./vendor/bin/phpunit
env:
DB_PORT: ${{ job.services.postgres.ports[5432] }}

46
.github/workflows/test.yml vendored Normal file
View File

@@ -0,0 +1,46 @@
name: Test App (MySQL)
on:
push:
branches:
- master
workflow_dispatch:
jobs:
test:
name: Test App (MySQL)
runs-on: ubuntu-latest
strategy:
matrix:
php: ['7.4', '8.0', '8.1', '8.2', '8.3', '8.4', '8.5']
steps:
- uses: actions/checkout@v3
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: mysql, mbstring, xml, imap, zip, gd, curl, intl, json
- name: Start MySQL
run: |
sudo systemctl start mysql
- name: Setup database
run: |
mysql -uroot -proot -e 'CREATE DATABASE IF NOT EXISTS `freescout-test`;'
mysql -uroot -proot -e "CREATE USER 'freescout-test'@'localhost' IDENTIFIED WITH mysql_native_password BY 'freescout-test';"
mysql -uroot -proot -e "GRANT ALL ON \`freescout-test\`.* TO 'freescout-test'@'localhost';"
mysql -uroot -proot -e 'FLUSH PRIVILEGES;'
- name: Install composer dependencies
run: composer install --ignore-platform-reqs --no-interaction
- name: Migrate and seed the database
run: |
php${{ matrix.php }} artisan migrate --force -n --database=testing
php${{ matrix.php }} artisan db:seed --force -n --database=testing
- name: Run PHP tests
if: ${{ matrix.php < 8.4 }}
run: php${{ matrix.php }} ./vendor/bin/phpunit

8
.gitignore vendored
View File

@@ -14,7 +14,7 @@ Homestead.yaml
npm-debug.log
yarn-error.log
.env
app/Console/Commands/Test*
/bootstrap/compiled.php
composer.phar
#composer.lock
@@ -28,8 +28,12 @@ Thumbs.db
/Modules/**/.git
/public/modules
/public/docs
/public/.dh-diag
/storage/.ignore_locales
/storage/.installed
/tools
.well-known
/resources/lang/module.*
/resources/lang/module.*
.phpunit.result.cache
/.phpunit.cache
.env.testing

3
DOCUMENTATION.md Normal file
View File

@@ -0,0 +1,3 @@
# Documentation
Documentation: https://github.com/freescout-help-desk/freescout/wiki

113
README.md
View File

@@ -2,95 +2,99 @@
<div align="center">
<img src="https://raw.githubusercontent.com/freescout-helpdesk/freescout/master/public/img/logo-300.png" width="180" height="180" />
<br/><br/>
[![PHP version](https://freescout-helpdesk.github.io/img/badges/PHP-7.1%2B-blue.svg)](https://github.com/freescout-helpdesk/freescout#requirements) [![Hits](https://hits.seeyoufarm.com/api/count/incr/badge.svg?url=https%3A%2F%2Fgithub.com%2Ffreescout-helpdesk%2Ffreescout&count_bg=%2379C83D&title_bg=%23555555&icon=&icon_color=%23E7E7E7&title=hits&edge_flat=false)](https://hits.seeyoufarm.com)
<img src="https://raw.githubusercontent.com/freescout-help-desk/freescout/master/public/img/logo-300.png" width="180" height="180" />
</div>
**FreeScout** is the super lightweight free open source help desk and shared inbox written in PHP (Laravel framework) – self hosted clone of HelpScout. Now you can enjoy free Zendesk & Help Scout without giving up privacy or locking yourself into a service you don't control. FreeScout has been developed from scratch and is not using any copyrighted Help Scout or Zendesk materials.
**FreeScout** is the super lightweight and powerful free open source help desk and shared inbox built with PHP (Laravel framework). Now you can enjoy free Zendesk & Help Scout without giving up privacy or locking yourself into a service you don't control. FreeScout has been developed from scratch and does not use any copyrighted Help Scout or Zendesk materials.
If you want to support the project feel free to **star this repository**. It helps to increase the visibility of the project and let people know that it is valuable. Thanks for your support!
![FreeScout](https://freescout-helpdesk.github.io/img/screenshots/screenshot.png)
## Table of Contents
* [Features](#features)
* [Demo](#demo)
* [Features](#features)
* [Mobile Apps](#mobile-apps)
* [Requirements](#requirements)
* [Installation](#installation)
* [Cloud Hosted](#cloud-hosted)
* [Images & One-Click Installs](#images--one-click-installs)
* [Modules](#modules)
* [Tools & Integrations](#tools--integrations)
* [News & Updates](#news--updates)
* [Contributing](#contributing)
* [Screenshots](#screenshots)
## Features
* No limitations on the number of users, tickets, mailboxes, etc.
* 100% Mobile-friendly.
* Multilingual (English, Croatian, Czech, Danish, Dutch, Finnish, French, German, Italian, Japanese, Korean, Norwegian, Persian, Polish, Portuguese, Russian, Spanish, Slovak, Swedish).
* Seamless email integration.
* Supports modern Microsoft Exchange authentication.
* Web installer & updater.
* Starred conversations.
* Forwarding conversations.
* Merging conversations.
* Moving conversations between mailboxes.
* Phone conversations.
* Sending new conversations to multiple recipients at once.
* Collision detection – notice is shown when two agents open the same conversation.
* Push notifications.
* Following a conversation.
* Auto reply.
* Internal notes.
* Automatic refreshing of the conversations list without the need to reload the page.
* Pasting screenshots from the clipboard into the reply area.
* Configuring notifications on a per user basis.
* Open tracking.
* Editing threads.
* Search.
Need anything else? Suggest features [here](https://freescout.net/request-feature/).
## Demo
**[Live Demo](https://demo.freescout.net)**
## Features
* Omnichannel — FreeScout centralizes in one place all customer conversations received by email, [WhatsApp](https://freescout.net/whatsapp/), Telegram, Facebook, Slack, Live Chat and more.
* [AI-powered](https://github.com/freescout-help-desk/AiIntegration) and super efficient.
* Unlimited support agents, tickets, mailboxes, etc.
* [Secure](https://freescout.net/security) by design.
* Mobile-friendly.
* Multilingual (translated into 33 languages).
* Seamless email integration (via SMTP, IMAP, POP3, etc).
* Modern OAuth authentication for Microsoft Office 365 and Google Workspace.
* Native [S3 storage](https://github.com/freescout-help-desk/S3Storage) support.
* Fully supports screen readers (for visually impaired).
* Web installer & updater.
* Starred conversations.
* Push notifications.
* Following conversations.
* Auto replies.
* Internal notes.
* Forwarding / Merging / Moving conversations.
* Phone conversations.
* Sending new conversations to multiple recipients at once.
* Collision detection – notice is shown when two agents open the same conversation.
* Automatic refreshing of the conversations list without the need to reload the page.
* Pasting screenshots from the clipboard into the reply area.
* Flexible notifications for user agents.
* Open tracking.
* Powerful search.
A full list of features is available [here](https://freescout.net/#custom_html-3).
Need anything else? [Suggest features](https://freescout.net/request-feature/).
## Mobile Apps
Mobile apps support the same functionality and modules as the web version of your FreeScout installation. Both support agents and administrators can use mobile apps.
<a href="https://freescout.net/android-app/" target="_blank" rel="nofollow"><img alt="Android App" src="https://freescout-helpdesk.github.io/img/apps/android.png" width="200px" /></a> <a href="https://freescout.net/ios-app/" target="_blank" rel="nofollow"><img alt="iOS App" src="https://freescout-helpdesk.github.io/img/apps/ios.png?v=1" width="200px" /></a>
[MacOS Menu Bar App](https://github.com/jonalaniz/scouter)
## Requirements
FreeScout is a pure PHP/MySQL application, so it can be easily deployed even on a shared hosting.
FreeScout is a pure PHP/MySQL application, so it can be easily deployed even on a [shared hosting](https://github.com/freescout-help-desk/freescout/wiki/Choosing-a-Server).
* Nginx / Apache / IIS
* PHP 7.1 - 8.2
* PHP 7.4 - 8.x
* MySQL 5.0+ / MariaDB 5.0+ / PostgreSQL
There is no minimum system requirements (CPU / RAM) – FreeScout will run on any system.
There are no minimum system requirements (CPU / RAM) – FreeScout will run on any system.
## Installation
[Installation Guide](https://github.com/freescout-helpdesk/freescout/wiki/Installation-Guide)
[Installation Guide](https://github.com/freescout-help-desk/freescout/wiki/Installation-Guide)
Images & one-click installs:
## Images & One-Click Installs
* [Docker Image](http://freescout.net/docker/)
* [Softaculous](http://www.softaculous.com/apps/customersupport/FreeScout) (cPanel, Plesk, ISPmanager, H-Sphere, DirectAdmin, InterWorx)
* [Fantastico](http://ff3.netenberg.com/visitors/scripts/freescout/view) (cPanel, DirectAdmin, ISP Manager, ISP Config)
* [Docker Image](https://freescout.net/docker/)
* [Pikapods](https://freescout.net/pikapods/) — Open-source apps hosting (Fully managed)
* [Zenith](https://freescout.net/zenith/) — Open-source apps hosting (Fully managed)
* [Hostinger](https://freescout.net/hostinger/) — VPS with automatic backups included
* [Softaculous](http://www.softaculous.com/apps/customersupport/FreeScout) — cPanel, Plesk, ISPmanager, H-Sphere, DirectAdmin, InterWorx
* [Fantastico](http://ff3.netenberg.com/visitors/scripts/freescout/view) — cPanel, DirectAdmin, ISP Manager, ISP Config
* [Cloudron](https://cloudron.io/store/net.freescout.cloudronapp.html)
* [Ubuntu](https://github.com/freescout-helpdesk/freescout/wiki/Installation-Guide#interactive-installation-bash-script-ubuntu) (bash script)
* [Ubuntu](https://github.com/freescout-help-desk/freescout/wiki/Installation-Guide#interactive-installation-bash-script-ubuntu) — Bash script
## Cloud Hosted
[Cloud Hosted FreeScout](https://freescout.net/cloud-hosted/)
[More one-click installs…](https://freescout.net/one-click-installs/)
## Modules
@@ -108,14 +112,14 @@ Images & one-click installs:
Don't miss news, updates and new modules!
[Email Newsletter](https://freescout.net/subscribe/) | [Facebook](https://freescout.net/facebook/) | [Twitter](https://freescout.net/twitter/) | [YouTube](https://freescout.net/youtube/) | [RSS](https://freescout.net/feed/)
[Email Newsletter](https://freescout.net/subscribe/) | [Facebook](https://freescout.net/facebook/) | [Twitter](https://freescout.net/twitter/) | [YouTube](https://freescout.net/youtube/) | [Telegram](https://freescout.net/telegram/) | [RSS](https://freescout.net/feed/)
## Contributing
* [Support the project by leaving a feedback](https://github.com/freescout-helpdesk/freescout/issues/288)
* [Development Guide](https://github.com/freescout-helpdesk/freescout/wiki/Development-Guide)
* [Todo list](https://github.com/freescout-helpdesk/freescout/labels/help%20wanted)
* [Translate](https://github.com/freescout-helpdesk/freescout/wiki/Translate)
* [Support the project by leaving a feedback](https://github.com/freescout-help-desk/freescout/issues/288)
* [Development Guide](https://github.com/freescout-help-desk/freescout/wiki/Development-Guide)
* [Todo list](https://github.com/freescout-help-desk/freescout/labels/help%20wanted)
* [Translate](https://github.com/freescout-help-desk/freescout/wiki/Translate)
## Screenshots
@@ -127,10 +131,9 @@ Conversation:
![Conversation](https://freescout-helpdesk.github.io/img/screenshots/conversation.png)
Drafting reply with AI:
Mailbox connection settings page:
![Mailbox connection settings page](https://freescout-helpdesk.github.io/img/screenshots/mailbox-connection.png)
![Mailbox connection settings page](https://freescout-helpdesk.github.io/img/screenshots/draft-reply-with-ai.png)
Notifications:

View File

@@ -1,5 +1,17 @@
# Security Policy
Read about **[FreeScout's Security](https://freescout.net/security/)**.
## Reporting a Vulnerability
Email at support@freescout.net
* Report a security vulnerability in the **Application Core**: https://github.com/freescout-help-desk/freescout/security/advisories
* Report a security vulnerability in a **Module**: https://freescout.net/contact-us/
## Guidelines
* One issue per Advisory — do not put several issues into one Advisory, as GitHub will not assign a CVE to such an Advisory.
* CVEs are requested after the fix for the vulnerability is released.
* If you found an issue related to the `APP_LIMIT_USER_CUSTOMER_VISIBILITY=true` mode, make sure to read the [FAQ](https://github.com/freescout-help-desk/freescout/wiki/FAQ#is-it-possible-to-have-separate-contactscustomers-per-mailbox).
* Support agents are allowed to move conversations to any mailbox, even to ones they don't have access to.
* Drafts are viewable/discardable by any member of the mailbox where they are created.
* FreeScout loads external images in received emails, and there is a Module allowing to disable loading external images: https://freescout.net/module/block-external-images/.

View File

@@ -19,9 +19,17 @@ class Attachment extends Model
const DIRECTORY = 'attachment';
CONST DISK = 'private';
const DISK = 'local_app';
CONST MIME_TYPE_MAX_LENGTH = 127;
const MIME_TYPE_MAX_LENGTH = 127;
// This token type was used for backward compatibility for some time
// and has been deprecated.
//const TOKEN_TYPE_LEGACY = 1;
// For backward compatibility.
const TOKEN_TYPE_MD5 = 2;
// Current way.
const TOKEN_TYPE_SHA256 = 3;
// https://github.com/Webklex/laravel-imap/blob/master/src/IMAP/Attachment.php
public static $types = [
@@ -37,7 +45,7 @@ class Attachment extends Model
];
public static $type_extensions = [
self::TYPE_VIDEO => ['flv', 'mp4', 'm3u8', 'ts', '3gp', 'mov', 'avi', 'wmv']
self::TYPE_VIDEO => ['flv', 'mp4', 'm3u8', 'ts', '3gp', 'mov', 'avi', 'wmv'],
];
public $timestamps = false;
@@ -53,7 +61,7 @@ class Attachment extends Model
/**
* Save attachment to file and database.
*/
public static function create($file_name, $mime_type, $type, $content, $uploaded_file, $embedded = false, $thread_id = null, $user_id = null)
public static function create($file_name, $mime_type, $type, $content, $uploaded_file, $embedded = false, $thread_id = null, $user_id = null, $upload_mode = \Helper::UPLOAD_MODE_DEFAULT)
{
if (!$content && !$uploaded_file) {
return false;
@@ -70,13 +78,13 @@ class Attachment extends Model
$orig_extension = pathinfo($file_name, PATHINFO_EXTENSION);
// Add underscore to the extension if file has restricted extension.
$file_name = \Helper::sanitizeUploadedFileName($file_name, $uploaded_file, $content);
$file_name = \Helper::sanitizeUploadedFileName($file_name, $uploaded_file, $content, $mime_type, $upload_mode);
// Replace some symbols in file name.
// Gmail can not load image if it contains spaces.
$file_name = preg_replace('/[ #\/]/', '-', $file_name);
$file_name = preg_replace('/[ #\/]/', '_', $file_name);
// Replace soft hyphens.
$file_name = str_replace(html_entity_decode('&#xAD;'), '-', $file_name);
$file_name = str_replace(html_entity_decode('&#xAD;'), '_', $file_name);
if (!$file_name) {
if (!$orig_extension) {
@@ -97,6 +105,11 @@ class Attachment extends Model
$file_name = 'RFC822.eml';
}
// https://github.com/freescout-helpdesk/freescout/issues/1412#issuecomment-1658881493
if ($file_name == 'undefined' && $mime_type == 'text/calendar') {
$file_name = 'calendar.ics';
}
if (strlen($file_name) > 255) {
$without_ext = pathinfo($file_name, PATHINFO_FILENAME);
$extension = pathinfo($file_name, PATHINFO_EXTENSION);
@@ -116,12 +129,13 @@ class Attachment extends Model
$attachment->mime_type = $mime_type;
$attachment->type = $type;
$attachment->embedded = $embedded;
$attachment->token_type = self::TOKEN_TYPE_SHA256;
$attachment->save();
$file_info = self::saveFileToDisk($attachment, $file_name, $content, $uploaded_file);
$attachment->file_dir = $file_info['file_dir'];
$attachment->size = Storage::disk(self::DISK)->size($file_info['file_path']);
$attachment->size = self::getDisk()->size($file_info['file_path']);
$attachment->save();
return $attachment;
@@ -129,6 +143,7 @@ class Attachment extends Model
/**
* Save file to the disk and return file_dir.
* $content may be a resource pointing to the file in remote storage.
*/
public static function saveFileToDisk($attachment, $file_name, $content, $uploaded_file)
{
@@ -141,17 +156,24 @@ class Attachment extends Model
do {
$i++;
$file_path = self::DIRECTORY.DIRECTORY_SEPARATOR.$file_dir.$i.DIRECTORY_SEPARATOR.$file_name;
} while (Storage::disk(self::DISK)->exists($file_path));
} while (self::getDisk()->exists($file_path));
$file_dir .= $i.DIRECTORY_SEPARATOR;
if ($uploaded_file) {
$uploaded_file->storeAs(self::DIRECTORY.DIRECTORY_SEPARATOR.$file_dir, $file_name, ['disk' => self::DISK]);
} else {
Storage::disk(self::DISK)->put($file_path, $content);
try {
if ($uploaded_file) {
$uploaded_file->storeAs(self::DIRECTORY.DIRECTORY_SEPARATOR.$file_dir, $file_name, self::getDiskName());
} else {
self::getDisk()->put($file_path, $content);
}
} catch (\Exception $e) {
\Helper::logException($e, '[Attachment::saveFileToDisk()]');
}
\Helper::sanitizeUploadedFileData($file_path, \Helper::getPrivateStorage(), $content);
// $content may be a stream resource rather than a string; the SVG sanitizer needs
// actual string content, so let it re-read the just-written file in that case instead
// of treating the resource as file content.
\Helper::sanitizeUploadedFileData($file_path, is_resource($content) ? null : $content);
return [
'file_dir' => $file_dir,
@@ -243,7 +265,18 @@ class Attachment extends Model
*/
public function url()
{
return Storage::url($this->getStorageFilePath()).'?id='.$this->id.'&token='.$this->getToken();
$file_path = $this->getStorageFilePath();
// URL must contain only forward slashes.
$file_path = str_replace(DIRECTORY_SEPARATOR, '/', $file_path);
$file_url = self::getDisk()->url($file_path);
// Fix percents.
// https://github.com/freescout-helpdesk/freescout/issues/3530
$file_url = str_replace('%', '%25', $file_url);
return $file_url.'?id='.$this->id.'&token='.$this->getToken();
}
/**
@@ -251,18 +284,23 @@ class Attachment extends Model
*/
public function getToken()
{
// \Hash::make() may contain . and / symbols which may cause problems.
return md5(config('app.key').$this->id.$this->size);
// https://github.com/freescout-help-desk/freescout/security/advisories/GHSA-89ww-mfww-5vr6
/*if ($this->token_type == self::TOKEN_TYPE_MD5) {
// Backward compatibility.
// \Hash::make() may contain . and / symbols which may cause problems.
return md5(config('app.key').$this->id.$this->size);
} else {*/
return hash_hmac('sha256', $this->id.$this->size.$this->file_name, config('app.key'));
}
/**
* Outputs the current Attachment as download
*/
public function download($view = false)
public function download($view = false, $headers = [])
{
$headers = [];
// #533
//return $this->getDisk()->download($this->getStorageFilePath(), \Str::ascii($this->file_name));
//return self::getDisk()->download($this->getStorageFilePath(), \Str::ascii($this->file_name));
if ($view) {
$headers['Content-Disposition'] = '';
}
@@ -272,11 +310,10 @@ class Attachment extends Model
$file_name = $file_name.'.eml';
}
return $this->getDisk()->download($this->getStorageFilePath(), $file_name, $headers);
}
// Cache attachments in browser - 1 month.
$headers['Cache-Control'] = 'max-age=2592000';
private function getDisk() {
return Storage::disk(self::DISK);
return self::getDisk()->download($this->getStorageFilePath(), $file_name, $headers);
}
/**
@@ -303,7 +340,7 @@ class Attachment extends Model
public function getLocalFilePath($full = true)
{
if ($full) {
return $this->getDisk()->path(self::DIRECTORY.DIRECTORY_SEPARATOR.$this->file_dir.$this->file_name);
return self::getDisk()->path(self::DIRECTORY.DIRECTORY_SEPARATOR.$this->file_dir.$this->file_name);
} else {
return DIRECTORY_SEPARATOR.'storage'.DIRECTORY_SEPARATOR.'app'.DIRECTORY_SEPARATOR.self::DIRECTORY.DIRECTORY_SEPARATOR.$this->file_dir.$this->file_name;
}
@@ -314,7 +351,7 @@ class Attachment extends Model
*/
public function fileExists()
{
return $this->getDisk()->exists(self::DIRECTORY.DIRECTORY_SEPARATOR.$this->file_dir.$this->file_name);
return self::getDisk()->exists(self::DIRECTORY.DIRECTORY_SEPARATOR.$this->file_dir.$this->file_name);
}
public static function formatBytes($size, $precision = 0)
@@ -365,7 +402,7 @@ class Attachment extends Model
{
// Delete from disk
foreach ($attachments as $attachment) {
$attachment->getDisk()->delete($attachment->getStorageFilePath());
self::getDisk()->delete($attachment->getStorageFilePath());
}
// Delete from DB
@@ -406,20 +443,31 @@ class Attachment extends Model
*/
public function duplicate($thread_id = null)
{
$new_attachment = $this->replicate();
if ($thread_id) {
$new_attachment->thread_id = $thread_id;
if (!$this->fileExists()) {
return null;
}
$new_attachment = $this->replicate();
$new_attachment->thread_id = $thread_id;
$new_attachment->save();
try {
$attachment_file = new \Illuminate\Http\UploadedFile(
$this->getLocalFilePath(), $this->file_name,
null, null, true
);
$content_stream = '';
$attachment_file = null;
$file_info = Attachment::saveFileToDisk($new_attachment, $new_attachment->file_name, '', $attachment_file);
if (\Helper::isLocalStorage()) {
$attachment_file = new \Illuminate\Http\UploadedFile(
$this->getLocalFilePath(), $this->file_name,
null, null, true
);
} else {
// File in remote storage.
$content_stream = $this->getFileStream();
}
$file_info = Attachment::saveFileToDisk($new_attachment, $new_attachment->file_name, $content_stream, $attachment_file);
if (!empty($file_info['file_dir'])) {
$new_attachment->file_dir = $file_info['file_dir'];
@@ -434,6 +482,41 @@ class Attachment extends Model
public function getFileContents()
{
return $this->getDisk()->get($this->getStorageFilePath());
try {
return self::getDisk()->get($this->getStorageFilePath());
} catch (\Exception $e) {
// File not found at path:...
\Helper::logException($e, '[Attachment::getFileContents()]');
return '';
}
}
/**
* Same as getFileContents(), but as a stream instead of loading the whole
* file into memory - use this when the bytes are only being copied
* elsewhere (e.g. duplicating/forwarding an attachment).
*/
public function getFileStream()
{
try {
return self::getDisk()->readStream($this->getStorageFilePath());
} catch (\Exception $e) {
\Helper::logException($e, '[Attachment::getFileStream()]');
return null;
}
}
public static function getDiskName()
{
if (\Helper::isLocalStorage()) {
return self::DISK;
} else {
return config('filesystems.default');
}
}
public static function getDisk()
{
return Storage::disk(self::getDiskName());
}
}

View File

@@ -56,6 +56,10 @@ class PolycastBroadcaster extends Broadcaster
*/
public function auth($request)
{
if (!$request->user()) {
throw new AccessDeniedHttpException();
}
// For connect request
if (empty($request->channels)) {
return true;
@@ -63,12 +67,9 @@ class PolycastBroadcaster extends Broadcaster
// Check all channels
foreach ($request->channels as $channel_name => $channel_info) {
// Copied from Illuminate\Broadcasting\Broadcasters\PusherBroadcaster
if (Str::startsWith($channel_name, ['private-', 'presence-']) &&
!$request->user()) {
throw new AccessDeniedHttpException();
}
// Copied from Illuminate\Broadcasting\Broadcasters\PusherBroadcaster
//if (Str::startsWith($channel_name, ['private-', 'presence-']) &&
if (Str::startsWith($channel_name, ['private-', 'presence-'])) {
$channelName = Str::startsWith($channel_name, 'private-')

View File

@@ -40,5 +40,7 @@ class AfterAppUpdate extends Command
$this->call('freescout:clear-cache');
$this->call('migrate', ['--force' => true]);
$this->call('queue:restart');
\Eventy::action('command.after_app_update');
}
}

View File

@@ -0,0 +1,48 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
class CleanNotificationsTable extends Command
{
const PERIOD = '-6 months';
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'freescout:clean-notifications-table';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Delete old read records from notifications table.';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
\DB::table('notifications')->where('created_at', '<', \Carbon\Carbon::now()->modify(self::PERIOD))
->whereNotNull('read_at')
->delete();
$this->info('['.date('Y-m-d H:i:s').'] Deleted old read notifications for: '.self::PERIOD);
}
}

View File

@@ -20,7 +20,7 @@ class CleanTmp extends Command
*
* @var string
*/
protected $description = 'Remove from system temp folder FreeScout files older than 1 week to avoid "No space left on device"';
protected $description = 'Remove from system temp folder old FreeScout tempt files to avoid "No space left on device"';
/**
* Create a new command instance.
@@ -39,7 +39,14 @@ class CleanTmp extends Command
*/
public function handle()
{
shell_exec('find '.\Helper::getTempDir().' -mtime +7 -type f -name '.\Helper::getTempFilePrefix().'* -exec rm -r -f {} \;');
// General cleaning.
// Remove 1 week old FreeScout files.
\Helper::shellExec('find '.\Helper::getTempDir().' -mtime +7 -type f -name '.\Helper::getTempFilePrefix().'* -exec rm -r -f {} \;');
// Remove temporary 1 day old SwiftMailer files.
// Example: /tmp/a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6/body
// https://github.com/freescout-help-desk/freescout/issues/5558
\Helper::shellExec('find '.\Helper::getTempDir().' -mtime +1 -type d -regextype posix-extended -regex ".*/[a-f0-9]{32}" -exec rm -r -f {} \;');
$this->comment("Done");
}

View File

@@ -39,6 +39,13 @@ class ClearCache extends Command
{
$this->call('clear-compiled');
$this->call('cache:clear');
// Remove files from /bootstrap/cache folder.
// https://github.com/freescout-help-desk/freescout/issues/4536
$files = new \Illuminate\Filesystem\Filesystem;
$files->delete($this->laravel->getCachedServicesPath());
$files->delete($this->laravel->getCachedPackagesPath());
$this->call('view:clear');
if ($this->option('doNotCacheConfig')) {
$this->call('config:clear');
@@ -50,10 +57,26 @@ class ClearCache extends Command
opcache_invalidate(app()->getCachedConfigPath());
}
}
// In FreeScout routes caching does not increase performance
// but it increases memory consumption and may lead to problems
// during application updating or installing/updating modules.
// try {
// $this->call('route:cache');
// } catch (\Exception $e) {
// // Do nothing.
// }
// Clear JS and CSS builds (generated in prod environment).
\Helper::shellExec('find '.public_path('js/builds').' -type f -name "*.js" ! -name "vars.js" -exec rm -r -f {} \;');
\Helper::shellExec('find '.public_path('css/builds').' -type f -name "*.css" -exec rm -r -f {} \;');
$this->info("Cleared: JS and CSS builds");
// Regenerate vars to get new data from .env
if (!$this->option('doNotGenerateVars')) {
$this->call('freescout:generate-vars');
}
// This should not be done during installation.
if (\Helper::isInstalled()) {
\Helper::queueWorkerRestart();

View File

@@ -48,8 +48,8 @@ class CreateUser extends Command
$user = new $class();
$user->role = $this->option('role');
if ($user->role ) {
if (!in_array($user->role , User::$roles)) {
if ($user->role) {
if (!in_array($user->role, User::$roles)) {
$this->error('Invalid role');
return false;
}

File diff suppressed because it is too large Load Diff

View File

@@ -49,24 +49,44 @@ class GenerateVars extends Command
//$filesystem = new Filesystem();
//$file_path = public_path('js/vars.js');
$file_path = storage_path('app/public/js/vars.js');
//$file_path = '/public/js/builds/vars.js';
$relative_path = 'js/builds/vars.js';
$file_path = public_path($relative_path);
$content = view('js/vars', $params)->render();
//$filesystem->put($file_path, $content);
// Save vars only if content changed
// Escape quotes in json values.
// https://github.com/freescout-help-desk/freescout/issues/4369
$content = preg_replace_callback(
"#(:[ ]*\")(.*)(\"[,\r\n])#",
function ($v) {
return $v[1].str_replace('"', '\"', $v[2]).$v[3];
},
$content
);
try {
if (\Storage::exists('js/vars.js')) {
$old_content = \Storage::get('js/vars.js');
if (!\File::exists(dirname($file_path))) {
\File::makeDirectory(dirname($file_path), \Helper::DIR_PERMISSIONS);
}
// Save vars only if content has changed.
if (\File::exists($file_path)) {
$old_content = \File::get($file_path);
if ($content != $old_content) {
\Storage::put('js/vars.js', $content);
\File::put($file_path, $content);
}
} else {
\Storage::put('js/vars.js', $content);
\File::put($file_path, $content);
}
$this->info("Created: ".substr($file_path, strlen(base_path())+1));
// Backward compatibility.
// Before vars.js was stored in /storage/app/public/js/vars.js
\Storage::put('js/vars.js', $content);
$this->info("Created: /".$relative_path);
} catch (\Exception $e) {
$msg = "Error occurred saving /storage/app/public/js/vars.js. ".\Helper::formatException($e);
$msg = "Error occurred saving $file_path. ".\Helper::formatException($e);
\Log::error($msg);
$this->error($msg);
}

View File

@@ -20,6 +20,35 @@ class LogsMonitor extends Command
*/
protected $description = 'Send new log records by email';
/**
* Default minimum identical-error occurrences required for
* `fetch_errors` entries to be included in the digest. Used when
* the `alert_logs_fetch_min_occurrences` option is not set. The
* default is 1, which matches upstream behaviour (no filtering);
* raise it via the alerts settings page to suppress transient
* single-shot fetch errors.
*/
const FETCH_ERRORS_MIN_OCCURRENCES_DEFAULT = 1;
/**
* Substrings that mark a `fetch_errors` row as transient. Rows
* matching any of these are dropped regardless of count, unless
* they appear at least the configured min-occurrences times.
*/
const FETCH_ERRORS_TRANSIENT_PATTERNS = [
'connection setup failed',
'failed to fetch messages',
'failed to authenticate',
'Connection closed',
'Connection reset',
'Connection timed out',
'Connection refused',
'stream_socket_client',
'SSL operation failed',
'Temporary failure',
'no response',
];
/**
* Create a new command instance.
*
@@ -62,6 +91,8 @@ class LogsMonitor extends Command
->where('created_at', '<', $now->toDateTimeString())
->get();
$logs = $this->filterTransientFetchErrors($logs);
if (!count($logs)) {
$this->line('['.date('Y-m-d H:i:s').'] No new log records found for the last '.$options['alert_logs_period']);
@@ -72,18 +103,18 @@ class LogsMonitor extends Command
$text = 'Logs having new records for the last '.$options['alert_logs_period'].':<ul>';
foreach ($names as $name) {
$text .= '<li>';
$text .= '<strong>'.\App\ActivityLog::getLogTitle($name).'</strong>';
$text .= '<strong>'.htmlspecialchars(\App\ActivityLog::getLogTitle($name)).'</strong>';
$text .= '</li>';
}
$text .= '</ul>';
foreach ($names as $name) {
$text .= '<br/><strong>'.\App\ActivityLog::getLogTitle($name).'</strong><br/>';
$text .= '<br/><strong>'.htmlspecialchars(\App\ActivityLog::getLogTitle($name)).'</strong><br/>';
foreach ($logs as $log) {
if ($log->log_name != $name) {
continue;
}
$text .= '● ['.$log->created_at.'] '.$log->getEventDescription().' <code>'.$log->properties.'</code><br/>';
$text .= '● ['.$log->created_at.'] '.htmlspecialchars($log->getEventDescription()).' <code>'.htmlspecialchars($log->properties).'</code><br/>';
}
}
// Send alert.
@@ -93,4 +124,87 @@ class LogsMonitor extends Command
$this->info('['.date('Y-m-d H:i:s').'] Monitoring finished');
}
/**
* Drop transient single-shot fetch errors. A fetch_errors row is
* considered transient when its error message matches one of the
* known transient patterns AND the same message appears fewer
* than the configured min-occurrences threshold within the window.
*/
protected function filterTransientFetchErrors($logs)
{
$threshold = (int) \Option::get(
'alert_logs_fetch_min_occurrences',
self::FETCH_ERRORS_MIN_OCCURRENCES_DEFAULT
);
if ($threshold <= 1) {
return $logs;
}
$fetchLogName = \App\ActivityLog::NAME_EMAILS_FETCHING;
$counts = [];
foreach ($logs as $log) {
if ($log->log_name !== $fetchLogName) {
continue;
}
$key = $this->fetchErrorFingerprint($log);
$counts[$key] = ($counts[$key] ?? 0) + 1;
}
$kept = [];
$dropped = 0;
foreach ($logs as $log) {
if ($log->log_name === $fetchLogName) {
$message = $this->fetchErrorMessage($log);
if ($this->isTransientFetchError($message)) {
$key = $this->fetchErrorFingerprint($log);
if (($counts[$key] ?? 0) < $threshold) {
$dropped++;
continue;
}
}
}
$kept[] = $log;
}
if ($dropped > 0) {
$this->line('['.date('Y-m-d H:i:s').'] Suppressed '.$dropped.' transient fetch_errors entries (threshold '.$threshold.')');
}
return collect($kept);
}
protected function fetchErrorMessage($log): string
{
$props = $log->properties;
if (is_object($props) && method_exists($props, 'toArray')) {
$props = $props->toArray();
} elseif (is_string($props)) {
$decoded = json_decode($props, true);
$props = is_array($decoded) ? $decoded : [];
}
return (string) ($props['error'] ?? '');
}
protected function fetchErrorFingerprint($log): string
{
$message = $this->fetchErrorMessage($log);
// Strip the trailing "; File: /path (line)" so the same error
// from different stack frames still groups together.
$message = preg_replace('/;\s*File:.*$/s', '', $message);
return trim($message);
}
protected function isTransientFetchError(string $message): bool
{
$lower = strtolower($message);
foreach (self::FETCH_ERRORS_TRANSIENT_PATTERNS as $needle) {
if (strpos($lower, strtolower($needle)) !== false) {
return true;
}
}
return false;
}
}

View File

@@ -21,7 +21,7 @@ class ModuleInstall extends Command
*
* @var string
*/
protected $description = 'Install module or all modules (if module_alias is empty)';
protected $description = 'Install module or all modules (if module_alias is empty): run migrations and create a symlink';
/**
* Create a new command instance.
@@ -86,25 +86,53 @@ class ModuleInstall extends Command
$this->call('freescout:clear-cache');
}
// There is similar function in \App\Module.
public function createModulePublicSymlink($module)
{
$from = public_path('modules').DIRECTORY_SEPARATOR.$module->alias;
$to = $module->getExtraPath('Public');
// Symlimk may exist but lead to the module folder in a wrong case.
// So we need first try to remove it.
if (!file_exists($from)) {
@unlink($from);
}
if (file_exists($from)) {
return $this->info('Public symlink already exists');
}
// file_exists() may throw "open_basedir restriction in effect".
try {
symlink($to, $from);
// If module's Public is symlink.
if (is_link($to)) {
@unlink($to);
}
// Symlimk may exist but lead to the module folder in a wrong case.
// So we need first try to remove it.
if (!file_exists($from) || !is_link($from)) {
if (is_dir($from)) {
@rename($from, $from.'_'.date('YmdHis'));
} else {
@unlink($from);
}
}
if (file_exists($from)) {
return $this->info('Public symlink already exists');
}
// Check target.
if (!file_exists($to)) {
// Try to create Public folder.
try {
\File::makeDirectory($to, \Helper::DIR_PERMISSIONS);
} catch (\Exception $e) {
// If it's a broken symlink.
if (is_link($to)) {
@unlink($to);
}
}
}
try {
symlink($to, $from);
} catch (\Exception $e) {
$this->error('Error occurred creating ['.$from.' » '.$to.'] symlink: '.$e->getMessage());
}
} catch (\Exception $e) {
$this->error('Error occurred creating ['.$from.'] symlink: '.$e->getMessage());
$this->error('Error occurred creating ['.$from.' » '.$to.'] symlink: '.$e->getMessage());
}
$this->info('The ['.$from.'] symlink has been created');

View File

@@ -21,6 +21,8 @@ class ModuleLaroute extends Command
*/
protected $description = 'Generate a laravel routes JS-file for a module or all modules (if module_alias is empty)';
public $routes;
public $config;
public $generator;

View File

@@ -0,0 +1,96 @@
<?php
/**
* php artisan module:migrate modulename.
*
* Replaces the command from nwidart/laravel-modules, which passes the module's
* migrations folder to the migrate command after removing base_path() from it.
* Module::getPath() is a realpath(), so on installations where the Modules
* folder is a symlink leading outside of the application folder (a common
* container layout: /var/www/html/Modules -> /data/Modules) base_path() does
* not occur in that path. The value stays absolute, migrate prefixes
* base_path() to it anyway (Illuminate\Database\Console\Migrations\
* BaseCommand::getMigrationPaths()), the resulting folder does not exist and
* the migration reports "Nothing to migrate" with a zero exit code.
*
* https://github.com/freescout-help-desk/freescout/issues/5569
*/
namespace App\Console\Commands;
use Nwidart\Modules\Commands\MigrateCommand;
use Nwidart\Modules\Migrations\Migrator;
use Nwidart\Modules\Module;
class ModuleMigrate extends MigrateCommand
{
/**
* Run the migrations of the specified module.
*
* @param Module $module
*/
protected function migrate(Module $module)
{
$path = $this->getMigrationPath($module);
if ($this->option('subpath')) {
$path = $path.'/'.$this->option('subpath');
}
$this->call('migrate', [
'--path' => $path,
'--database' => $this->option('database'),
'--pretend' => $this->option('pretend'),
'--force' => $this->option('force'),
]);
if ($this->option('seed')) {
$this->call('module:seed', ['module' => $module->getName()]);
}
}
/**
* Get the module's migrations folder relative to base_path(), as the
* migrate command prepends base_path() to whatever --path receives.
*
* @param Module $module
*
* @return string
*/
protected function getMigrationPath(Module $module)
{
$absolute_path = (new Migrator($module))->getPath();
$path = str_replace(base_path(), '', $absolute_path);
if ($path != $absolute_path) {
// The module is located inside of the application folder.
return $path;
}
// The module path is a realpath() leading outside of the application
// folder. Rebuild the path from the folder the module has been scanned
// from, which still contains the symlink and is therefore located
// inside of the application folder.
$module_path = $module->getPath();
if (method_exists($module, 'getScannedPath')
&& $module_path
&& strpos($absolute_path, $module_path) === 0
) {
$scanned_path = str_replace(base_path(), '', $module->getScannedPath());
$scanned_path .= substr($absolute_path, strlen($module_path));
if (is_dir(base_path().DIRECTORY_SEPARATOR.$scanned_path)) {
return $scanned_path;
}
}
// Neither path is located inside of the application folder, so the
// migrate command can not resolve it. Tell the user instead of
// reporting "Nothing to migrate".
$this->error('Module migrations folder is located outside of the application folder: '.$absolute_path);
$this->error('Run "php artisan migrate" to apply the module migrations.');
return $path;
}
}

View File

@@ -0,0 +1,169 @@
<?php
/**
* php artisan freescout:module-install modulealias.
*/
namespace App\Console\Commands;
use Illuminate\Console\Command;
class ModuleUpdate extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'freescout:module-update {module_alias?}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Update all modules or a single module (if module_alias is set)';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$install_all = false;
$modules = [];
// We have to clear modules cache first to update modules cache
\Artisan::call('cache:clear');
// Create a symlink for the module (or all modules)
$module_alias = $this->argument('module_alias');
$modules_directory = \WpApi::getModules();
if (\WpApi::$lastError) {
$this->error(__('Error occurred').': '.$lastError['message'].' ('.$lastError['code'].')');
return;
}
$installed_modules = \Module::all();
$counter = 0;
$found = false;
foreach ($modules_directory as $dir_module) {
// Update single module.
if ($module_alias && $dir_module['alias'] != $module_alias) {
continue;
}
$found = true;
// Detect if new version is available.
foreach ($installed_modules as $module) {
if ($module->getAlias() != $dir_module['alias'] /*|| !$module->active()*/) {
continue;
}
if (!empty($dir_module['version']) && version_compare($dir_module['version'], $module->get('version'), '>')) {
$update_result = \App\Module::updateModule($dir_module['alias']);
$this->info('['.$update_result['module_name'].' Module'.']');
if ($update_result['status'] == 'success') {
$this->line($update_result['msg_success']);
} else {
$msg = $update_result['msg'];
if ($update_result['download_msg']) {
$msg .= ' ('.$update_result['download_msg'].')';
}
$this->error('ERROR: '.$msg);
}
if (trim($update_result['output'])) {
$this->line(preg_replace("#\n#", "\n> ", '> '.trim($update_result['output'])));
}
$counter++;
}
}
}
// Update custom modules.
// Loop through each installed module.
foreach ($installed_modules as $module) {
// Skip if the module is an official one
if (\App\Module::isOfficial($module->get('authorUrl'))) {
continue;
}
// Get the URL for the latest version of the module
$latest_version_number_url = $module->get('latestVersionUrl');
if (! $latest_version_number_url) {
continue;
}
// Create a new Guzzle HTTP client
$client = new \GuzzleHttp\Client();
try {
// Send a GET request to the latest version URL
$response = $client->request('GET', $latest_version_number_url, \Helper::setGuzzleDefaultOptions());
// Get the latest version number from the response body
$latest_version = trim((string) $response->getBody());
if (empty($latest_version)) {
continue;
} else {
// Get the current version of the module
$current_version = $module->get('version');
}
} catch (\Exception $e) {
// If there's an exception, skip to the next iteration
continue;
}
// If the latest version is greater than the current version
if (version_compare($latest_version, $current_version, '>')) {
// Update the module
$update_result = \App\Module::updateModule($module->getAlias());
// Print the module name and status
$this->info('[' . $update_result['module_name'] . ' Module' . ']');
if ($update_result['status'] == 'success') {
// If the update was successful, print the success message
$this->line($update_result['msg_success']);
} else {
// If the update failed, print the error message
$msg = $update_result['msg'];
if ($update_result['download_msg']) {
$msg .= ' (' . $update_result['download_msg'] . ')';
}
$this->error('ERROR: ' . $msg);
}
// If there's any output from the update, print it
if (trim($update_result['output'])) {
$this->line(preg_replace("#\n#", "\n> ", '> ' . trim($update_result['output'])));
}
// Increment the counter
$counter ++;
}
}
if ($module_alias && !$found) {
$this->error('Module with the following alias not found: '.$module_alias);
} elseif (!$counter) {
$this->line('All modules are up-to-date');
}
\Artisan::call('freescout:clear-cache');
}
}

View File

@@ -0,0 +1,126 @@
<?php
namespace App\Console\Commands;
use App\Mailbox;
use Webklex\PHPIMAP\IMAP;
use Webklex\PHPIMAP\Message;
use Illuminate\Console\Command;
class ParseEml extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'freescout:parse-eml';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Parse EML file';
/**
* Current mailbox.
*
* Used to process emails sent to multiple mailboxes.
*/
public $mailbox;
/**
* Used to process emails sent to multiple mailboxes.
*/
public $mailboxes;
public $extra_import = [];
/**
* Page size when requesting emails from mail server.
*/
const PAGE_SIZE = 300;
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$email = file_get_contents(storage_path('logs/email.eml'));
if (!str_contains($email, "\r\n")) {
$email = str_replace("\n", "\r\n", $email);
}
// $raw_header = substr($email, 0, strpos($email, "\r\n\r\n"));
// $raw_body = substr($email, strlen($raw_header)+4);
// $mailbox = Mailbox::find($this->option('mailbox'));
// //\Config::set('app.new_fetching_library', 'true');
// $client = \MailHelper::getMailboxClient($mailbox);
// $client->openFolder("INBOX");
// $message = Message::make(/*$this->option('uid')*/null, null, $client, $raw_header, $raw_body, [/*0 => "\\Seen"*/], IMAP::ST_UID);
$manager = new \Webklex\PHPIMAP\ClientManager([
// 'options' => [
// "debug" => $_ENV["LIVE_MAILBOX_DEBUG"] ?? false,
// ],
// 'accounts' => [
// 'default' => [
// 'host' => getenv("LIVE_MAILBOX_HOST"),
// 'port' => getenv("LIVE_MAILBOX_PORT"),
// 'encryption' => getenv("LIVE_MAILBOX_ENCRYPTION"),
// 'validate_cert' => getenv("LIVE_MAILBOX_VALIDATE_CERT"),
// 'username' => getenv("LIVE_MAILBOX_USERNAME"),
// 'password' => getenv("LIVE_MAILBOX_PASSWORD"),
// 'protocol' => 'imap', //might also use imap, [pop3 or nntp (untested)]
// ],
// ],
]);
$message = \Webklex\PHPIMAP\Message::fromString($email);
$this->line('Headers: ');
$this->info($message->getHeader()->raw);
$this->line('From: ');
$this->info(json_encode($message->getFrom()[0] ?? [], JSON_UNESCAPED_UNICODE));
$this->line('Reply-To: ');
$this->info(json_encode($message->getReplyTo()[0] ?? [], JSON_UNESCAPED_UNICODE));
$this->line('In-Reply-To: ');
$this->info($message->getInReplyTo());
$this->line('References: ');
$this->info(json_encode(array_values(array_filter(preg_split('/[, <>]/', $message->getReferences() ?? ''))), JSON_UNESCAPED_UNICODE));
$this->line('Date: ');
$this->info($message->getDate());
$this->line('Subject: ');
$this->info($message->getSubject());
$this->line('Text Body: ');
$this->info($message->getTextBody());
$this->line('HTML Body: ');
$html_body = $message->getHTMLBody(false) ?? '';
$this->info($html_body);
$attachments = $message->getAttachments();
if (count($attachments)) {
$this->line('Attachments: ');
foreach ($attachments as $attachment) {
$this->info('— '.$attachment->getName().(strstr($html_body, 'cid:'.$attachment->id) ? ' (embedded)' : ''));
}
}
}
}

View File

@@ -43,7 +43,7 @@ class SendMonitor extends Command
// Get SendReplyToCustomer jobs.
$pending_jobs = \App\Job::where('queue', 'emails')
->where('payload', 'like', '{"displayName":"App\\\\\\\\Jobs\\\\\\\\SendReplyToCustomer"%')
->where('created_at', '<', time() - self::CHECK_PERIOD)
->where('available_at', '<', time() - self::CHECK_PERIOD)
->exists();
// Check failed_jobs.

View File

@@ -10,8 +10,6 @@ use App\Option;
class Kernel extends ConsoleKernel
{
const FETCH_MAX_EXECUTION_TIME = 30; // minutes
/**
* The Artisan commands provided by your application.
*
@@ -32,7 +30,10 @@ class Kernel extends ConsoleKernel
*/
protected function schedule(Schedule $schedule)
{
// Keep in mind that this function is also called on clearing cache.
// https://github.com/freescout-helpdesk/freescout/issues/3970
if (!$this->isScheduleRun() && !\Helper::isRoute('system.cron')) {
return;
}
// Remove failed jobs
$schedule->command('queue:flush')
@@ -69,6 +70,9 @@ class Kernel extends ConsoleKernel
$schedule->command('freescout:clean-send-log')
->monthly();
$schedule->command('freescout:clean-notifications-table')
->weekly();
$schedule->command('freescout:clean-tmp')
->daily();
@@ -97,22 +101,32 @@ class Kernel extends ConsoleKernel
}
}
$fetch_unseen = (int)config('app.fetch_unseen');
$fetch_command_identifier = \Helper::getWorkerIdentifier('freescout:fetch-emails');
$fetch_command_name = 'freescout:fetch-emails'
. ' --identifier='.$fetch_command_identifier
. ' --unseen='.$fetch_unseen;
// Kill fetch commands running for too long.
// In shedule:run this code is executed every time $schedule->command() in this function is executed.
if ($this->isScheduleRun() && function_exists('shell_exec')) {
if (function_exists('shell_exec')) {
$fetch_command_pids = \Helper::getRunningProcesses($fetch_command_identifier);
$mutex_name = $schedule->command('freescout:fetch-emails')
->skip(function () {
return true;
})
->mutexName();
// If there is no cache mutext but there are running fetch commands
// it means the mutex had expired after self::FETCH_MAX_EXECUTION_TIME
// and the existing command(s) is running longer than self::FETCH_MAX_EXECUTION_TIME.
// The name of the command here must be exactly the same as below!
// Otherwise long fetching will be killed and won't run longer than 1 mintue.
// $schedule->command() creates a new mutex, so we need to store mutex name in cache.
// $mutex_name = $schedule->command($fetch_command_name)
// ->everyMinute() - this also need to be set
// ->skip(function () {
// return true;
// })
// ->mutexName();
$mutex_name = \Cache::get('fetch_mutex_name') ?? '';
// If there is no cache mutext but there are running fetch commands
// it means the mutex had expired after 'fetch_max_execution_time'
// and the existing command(s) is running longer than 'fetch_max_execution_time'.
if (count($fetch_command_pids) > 0 && !\Cache::get($mutex_name)) {
// Kill freescout:fetch-emails commands running for too long
shell_exec('kill '.implode(' | kill ', $fetch_command_pids));
@@ -125,22 +139,33 @@ class Kernel extends ConsoleKernel
// So here we are forcefully removing the mutex. Otherwise mutex will live for 24 hours.
if (\Cache::has($mutex_name)) {
\Cache::forget($mutex_name);
// We could not remove the mutex from cache - something is wrong with permissions.
if (\Cache::has($mutex_name)) {
\Log::error('[schedule:run] Could not remove fetch mutex from cache (cache key: '.$mutex_name.'). Check file permissions.');
}
}
}
}
}
// Fetch emails from mailboxes
$fetch_command = $schedule->command('freescout:fetch-emails --identifier='.$fetch_command_identifier)
// withoutOverlapping() option creates a mutex in the cache
$fetch_command = $schedule->command($fetch_command_name)
// withoutOverlapping() option creates a mutex in the cache
// which by default expires in 24 hours.
// So we are passing an 'expiresAt' parameter to withoutOverlapping() to
// prevent fetching from not being executed when fetching command by some reason
// does not remove the mutex from the cache.
->withoutOverlapping($expiresAt = self::FETCH_MAX_EXECUTION_TIME /* minutes */)
// does not remove the mutex from the cache.
->withoutOverlapping($expiresAt = (int)config('app.fetch_max_execution_time') /* minutes */)
->sendOutputTo(storage_path().'/logs/fetch-emails.log');
switch (config('app.fetch_schedule')) {
case Mail::FETCH_SCHEDULE_EVERY_TWO_MINUTES:
$fetch_command->cron('*/2 * * * *');
break;
case Mail::FETCH_SCHEDULE_EVERY_THREE_MINUTES:
$fetch_command->cron('*/3 * * * *');
break;
case Mail::FETCH_SCHEDULE_EVERY_FIVE_MINUTES:
$fetch_command->everyFiveMinutes();
break;
@@ -160,6 +185,7 @@ class Kernel extends ConsoleKernel
$fetch_command->everyMinute();
break;
}
\Cache::put('fetch_mutex_name', $fetch_command->mutexName(), (int)config('app.fetch_max_execution_time'));
$schedule = \Eventy::filter('schedule', $schedule);
@@ -181,10 +207,10 @@ class Kernel extends ConsoleKernel
// $schedule->command('queue:work') command below has withoutOverlapping() option,
// which works via special mutex stored in the cache preventing several 'queue:work' to work at the same time.
// So when the cache is cleared the mutex indicating that the 'queue:work' is running is removed,
// and the second 'queue:work' command is launched by cron. When `artisan schedule:run` is executed it sees
// and the second 'queue:work' command is launched by cron. When `artisan schedule:run` is executed it sees
// that there are two 'queue:work' processes running and kills them.
// After one minute 'queue:work' is executed by cron via `artisan schedule:run` and works in the background.
if ($this->isScheduleRun() && function_exists('shell_exec')) {
if (function_exists('shell_exec')) {
$running_commands = \Helper::getRunningProcesses();
if (count($running_commands) > 1) {
@@ -196,7 +222,7 @@ class Kernel extends ConsoleKernel
sleep(1);
// Check processes again.
$worker_pids = \Helper::getRunningProcesses();
if (count($worker_pids) > 1) {
// Current process also has to be killed, as otherwise it "stucks"
// $current_pid = getmypid();
@@ -220,8 +246,14 @@ class Kernel extends ConsoleKernel
return true;
})
->mutexName();
if (\Cache::get($mutex_name)) {
if (\Cache::has($mutex_name)) {
\Cache::forget($mutex_name);
// We could not remove the mutex from cache - something is wrong with permissions.
if (\Cache::has($mutex_name)) {
\Log::error('[schedule:run] Could not remove queue:work mutex from cache (cache key: '.$mutex_name.'). Check file permissions.');
}
}
}
}
@@ -234,12 +266,12 @@ class Kernel extends ConsoleKernel
}
/**
* This function is needed because every time $schedule->command() is executed
* This function is needed because every time $schedule->command() is executed
* the schedule() is executed also.
*/
public function isScheduleRun()
{
if (!\Helper::isConsole()) {
if (\Helper::isConsole()) {
return true;
} else {
return !empty($_SERVER['argv']) && in_array('schedule:run', $_SERVER['argv']);

File diff suppressed because it is too large Load Diff

View File

@@ -3,6 +3,8 @@
namespace App;
use App\Email;
use App\CustomerChannel;
use App\Thread;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Storage;
@@ -453,7 +455,7 @@ class Customer extends Model
*
* @var [type]
*/
protected $fillable = ['first_name', 'last_name', 'company', 'job_title', 'address', 'city', 'state', 'zip', 'country', 'photo_url', 'age', 'gender', 'notes', 'channel', 'channel_id', 'social_profiles'];
protected $fillable = ['first_name', 'last_name', 'company', 'job_title', 'address', 'city', 'state', 'zip', 'country', 'photo_url', 'notes', 'channel', 'channel_id', 'social_profiles'];
/**
* Fields stored as JSON.
@@ -493,11 +495,27 @@ class Customer extends Model
}
/**
* Get main email.
* Get emails as string.
*/
public static function getMainEmailStatic($customer_id)
public function getEmailsAsArray()
{
return Email::select('email')->where('customer_id', $customer_id)->pluck('email');
return self::getCustomerEmailsAsArray($this->id);
}
/**
* Get emails as string.
*/
public static function getCustomerEmailsAsArray($customer_id)
{
return Email::select('email')->where('customer_id', $customer_id)->pluck('email')->toArray();
}
/**
* Check if the customer has an email address among his emails.
*/
public function hasEmail($email_address)
{
return $this->emails_cached()->where('email', Email::sanitizeEmail($email_address))->exists();
}
/**
@@ -641,9 +659,9 @@ class Customer extends Model
return $phones;
} elseif ($dummy_if_empty) {
return [[
'type' => self::PHONE_TYPE_WORK,
'value' => '',
]];
'type' => self::PHONE_TYPE_WORK,
],];
} else {
return [];
}
@@ -767,7 +785,7 @@ class Customer extends Model
return [[
'type' => '',
'value' => '',
]];
],];
} else {
return [];
}
@@ -813,7 +831,7 @@ class Customer extends Model
if (!$value || preg_match("/^http(s)?:?\/?\/?$/i", $value)) {
continue;
}
if (!preg_match("/http(s)?:\/\//i", $value)) {
if (!preg_match("/^http(s)?:\/\//i", $value)) {
$value = 'http://'.$value;
}
$websites[] = (string) $value;
@@ -866,12 +884,16 @@ class Customer extends Model
}
$social_profiles[] = [
// Order of elements in array is important as we rely on it
// when searching customers by social profiles using "like".
'value' => (string) $social_profile['value'],
'type' => $type,
];
}
} else {
$social_profiles[] = [
// Order of elements in array is important as we rely on it
// when searching customers by social profiles using "like".
'value' => (string) $social_profile,
'type' => self::SOCIAL_TYPE_OTHER,
];
@@ -919,6 +941,9 @@ class Customer extends Model
if (!$email) {
return null;
}
$email = Email::sanitizeLength($email);
$email_obj = Email::where('email', $email)->first();
if ($email_obj) {
$customer = $email_obj->customer;
@@ -940,7 +965,7 @@ class Customer extends Model
} else {
$customer = new self();
$email_obj = new Email();
$email_obj->email = $email;
$email_obj->email = Email::sanitizeLength($email);
$new = true;
}
@@ -996,6 +1021,9 @@ class Customer extends Model
$data['notes'] = $data['background'];
}
// Strip tags.
$data = \Helper::stripTagsFromArray($data, $this->fillable);
if ($replace_data) {
// Replace data.
$data_prepared = $data;
@@ -1008,6 +1036,14 @@ class Customer extends Model
$result = true;
} else {
// Update empty fields.
// Do not set last name if first name is already set (and vise versa).
if (!empty($this->first_name) && !empty($data['last_name'])) {
unset($data['last_name']);
}
if (!empty($this->last_name) && !empty($data['first_name'])) {
unset($data['first_name']);
}
foreach ($data as $key => $value) {
if (in_array($key, $this->fillable) && empty($this->$key)) {
$this->$key = $value;
@@ -1121,7 +1157,7 @@ class Customer extends Model
*/
public function url()
{
return route('customers.update', ['id'=>$this->id]);
return route('customers.update', ['id' => $this->id]);
}
/**
@@ -1131,7 +1167,7 @@ class Customer extends Model
*/
public function urlView()
{
return route('customers.conversations', ['id'=>$this->id]);
return route('customers.conversations', ['id' => $this->id]);
}
/**
@@ -1266,6 +1302,95 @@ class Customer extends Model
return Storage::url(self::PHOTO_DIRECTORY.DIRECTORY_SEPARATOR.$file_name);
}
/**
* Merge customerю
*/
public function mergeWith(Customer $customer2)
{
if ($this->id == $customer2->id) {
return false;
}
$user = auth()->user();
$customer2->conversations()->update(['customer_id' => $this->id]);
// do {
// $conversations = $customer2->conversations()->limit(1000);
// foreach ($conversations as $conversation) {
// $conversation->changeCustomer($customer_email, $this, $user);
// }
// } while (count($conversations) > 0);
// Move emails.
$customer2->emails()->update(['customer_id' => $this->id]);
// Update customer in threads.
// https://github.com/freescout-help-desk/freescout/issues/5505
Thread::where('customer_id', $customer2->id)->update(['customer_id' => $this->id]);
Thread::where('created_by_customer_id', $customer2->id)->update(['created_by_customer_id' => $this->id]);
// Merge attributes
// if (in_array('phone', $keepAttributes) && !$this->phone) {
// $this->phone = $customerToMerge->phone;
// }
foreach ($this->fillable as $attr_name) {
// Skip some attribues.
if (in_array($attr_name, ['channel', 'channel_id'])) {
continue;
}
if (!$this->$attr_name) {
$this->$attr_name = $customer2->$attr_name;
}
}
// Merge Phones.
$phones = self::mergeTypeValueLists($this->getPhones(), $customer2->getPhones());
if (count($phones) != count($this->getPhones())) {
$this->setPhones($phones);
}
// Merge websites.
$websites = array_merge($this->getWebsites(), $customer2->getWebsites());
$this->setWebsites(array_unique($websites));
// Merge social profiles.
$social = self::mergeTypeValueLists($this->getSocialProfiles(), $customer2->getSocialProfiles());
if (count($social) != count($this->getSocialProfiles())) {
$this->setSocialProfiles($social);
}
$this->save();
\Eventy::action('customer.merged', $this, $customer2, $user);
$customer2->delete();
return true;
}
public static function mergeTypeValueLists($list1, $list2)
{
foreach ($list2 as $data2) {
if (empty($data2['type']) || empty($data2['value'])) {
continue;
}
$exists = false;
foreach ($list1 as $data) {
if (!empty($data['type']) && !empty($data['value'])
&& $data['type'] == $data2['type'] && $data['value'] == $data2['value']
) {
$exists = true;
break;
}
}
if (!$exists) {
$list1[] = $data2;
}
}
return $list1;
}
/**
* Resize and save photo.
*/
@@ -1279,7 +1404,8 @@ class Customer extends Model
}
$file_name = md5(Hash::make($this->id)).'.jpg';
$dest_path = Storage::path(self::PHOTO_DIRECTORY.DIRECTORY_SEPARATOR.$file_name);
$storage_path = self::PHOTO_DIRECTORY.DIRECTORY_SEPARATOR.$file_name;
$dest_path = Storage::disk('local')->path($storage_path);
$dest_dir = pathinfo($dest_path, PATHINFO_DIRNAME);
if (!file_exists($dest_dir)) {
@@ -1293,6 +1419,8 @@ class Customer extends Model
imagejpeg($resized_image, $dest_path, self::PHOTO_QUALITY);
$file_name = \Eventy::filter('customer.save_photo', $file_name, $this, $dest_path, $storage_path);
return $file_name;
}
@@ -1327,10 +1455,23 @@ class Customer extends Model
return $data;
}
$name_parts = explode(' ', $name, 2);
$data['first_name'] = $name_parts[0];
if (!empty($name_parts[1])) {
$data['last_name'] = $name_parts[1];
if (strstr($name, ',')) {
// Smith, John.
// https://github.com/freescout-help-desk/freescout/issues/5074
$name_parts = explode(',', $name, 2);
if (!empty($name_parts[1]) && trim($name_parts[1])) {
$data['first_name'] = trim($name_parts[1]);
$data['last_name'] = trim($name_parts[0]);
} else {
$data['first_name'] = trim($name_parts[0]);
}
} else {
// Normal format.
$name_parts = explode(' ', $name, 2);
$data['first_name'] = $name_parts[0];
if (!empty($name_parts[1])) {
$data['last_name'] = $name_parts[1];
}
}
return $data;
@@ -1366,13 +1507,31 @@ class Customer extends Model
public function setPhotoFromRemoteFile($url)
{
$headers = get_headers($url);
// Sanitize URL.
/*try {
\Helper::sanitizeRemoteUrl($url, true);
} catch (\Exception $e) {
if ($e->getCode() == \Helper::EXCEPTION_UNSAFE_URL) {
\Helper::logException($e, 'Customer::setPhotoFromRemoteFile()');
return false;
} else {
throw $e;
}
}*/
// Quick check for response code 200.
try {
$headers = get_headers($url);
} catch (\Exception $e) {
\Helper::logException($e, 'Customer::setPhotoFromRemoteFile()');
return false;
}
if (!preg_match("/200/", $headers[0])) {
return false;
}
$image_data = \Helper::getRemoteFileContents($url);
// URL sanitizing takes place in getRemoteFileContents().
$image_data = \Helper::getRemoteFileContents($url, $follow_redirects = false);
if (!$image_data) {
return false;
@@ -1446,4 +1605,127 @@ class Customer extends Model
Email::where('customer_id', $this->id)->delete();
$this->delete();
}
public function getChannels()
{
if (!$this->channel || !$this->channel_id) {
return collect([]);
}
return CustomerChannel::where('customer_id', $this->id)->get();
}
public function addChannel($channel, $channel_id)
{
// We are doing this to let existing modules not to throw error
// and as a flag that this customer has record(s) in cucstomer_channel table.
if (!$this->channel || !$this->channel_id) {
$this->channel = $channel;
$this->channel_id = $channel_id;
$this->save();
}
$customer_channel = CustomerChannel::where('customer_id', $this->id)
->where('channel', $channel)
->first();
if ($customer_channel) {
// Update channel_id.
if ($customer_channel->channel_id != $channel_id) {
try {
$customer_channel->channel_id = $channel_id;
$customer_channel->save();
} catch (\Exception $e) {
// Do nothing.
}
return $customer_channel;
}
} else {
// Create.
return CustomerChannel::create($this->id, $channel, $channel_id);
}
}
// Each customer can have only one record for specific CHANNEL in CustomerChannel table.
public function updateChannelId($channel, $new_channel_id)
{
if (!$channel || !$new_channel_id) {
return;
}
try {
CustomerChannel::where('customer_id', $this->id)
->where('channel', $channel)
->update(['channel_id' => $new_channel_id]);
} catch (\Exception $e) {
// Do nothing.
}
}
public static function getCustomerByChannel($channel, $channel_id)
{
$customer_channel = CustomerChannel::where('channel', $channel)
->where('channel_id', $channel_id)
->first();
if ($customer_channel) {
return $customer_channel->customer;
} else {
return null;
}
}
public function getChannelId($channel)
{
return CustomerChannel::where('customer_id', $this->id)
->where('channel', $channel)
->value('channel_id');
}
public static function findCustomersBySocialProfile($type, $value, $exclude_channel = null)
{
$value = mb_strtolower($value);
$like = '%'.$value.'","type":'.$type.'}]';
$customers = Customer::where('social_profiles', \Helper::sqlLikeOperator(), $like)->get();
// Now more prcise filtering.
foreach ($customers as $i => $customer) {
$ok = false;
foreach ($customer->getSocialProfiles() as $social_profile) {
if ($social_profile['type'] == $type
// Try to check username written in different ways:
// - username
// - @username
// - https://example.org/username
&& preg_match("#(^|/|@)".preg_quote($value)."$#", trim(mb_strtolower($social_profile['value'])))
) {
$ok = true;
break;
}
}
if (!$ok) {
$customers->forget($i);
}
}
if ($exclude_channel && count($customers)) {
$exclude_customer_ids = CustomerChannel::whereIn('customer_id', $customers->pluck('id'))
->where('channel', $exclude_channel)
->pluck('customer_id');
return $customers->whereNotIn('customer_id', $exclude_customer_ids);
} else {
return $customers;
}
}
/**
* Get dummy customer.
*/
public static function getDummyCustomer()
{
$customer = new self();
$customer->first_name = __('Customer');
$customer->last_name = '';
return $customer;
}
}

58
app/CustomerChannel.php Normal file
View File

@@ -0,0 +1,58 @@
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class CustomerChannel extends Model
{
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'customer_channel';
public $timestamps = false;
/**
* Cached communication channels.
*/
public static $channels = null;
public function customer()
{
return $this->belongsTo('App\Customer');
}
public static function create($customer_id, $channel, $channel_id)
{
try {
$customer_channel = new self();
$customer_channel->customer_id = $customer_id;
$customer_channel->channel = $channel;
$customer_channel->channel_id = $channel_id;
$customer_channel->save();
return $customer_channel;
} catch (\Exception $e) {
// Already exists.
return null;
}
}
public function getChannelName()
{
return \Eventy::filter('channel.name', '', $this->channel);
}
public static function getChannels()
{
if (self::$channels !== null) {
return self::$channels;
} else {
self::$channels = \Eventy::filter('channels.list', []);
return self::$channels;
}
}
}

View File

@@ -18,6 +18,8 @@ class Email extends Model
const TYPE_HOME = 2;
const TYPE_OTHER = 3;
const MAX_LENGTH = 191;
public static $types = [
self::TYPE_WORK => 'work',
self::TYPE_HOME => 'home',
@@ -31,6 +33,14 @@ class Email extends Model
*/
protected $guarded = ['id', 'customer_id'];
public function __construct(array $attributes = [])
{
if (!empty($attributes['email'])) {
$attributes['email'] = self::sanitizeLength($attributes['email']);
}
parent::__construct($attributes);
}
/**
* Get email's customer.
*/
@@ -84,4 +94,9 @@ class Email extends Model
return null;
}
}
public static function sanitizeLength($email_address)
{
return substr($email_address, Email::MAX_LENGTH*(-1));
}
}

View File

@@ -15,7 +15,7 @@ class CustomerCreatedConversation
*
* @return void
*/
public function __construct(Conversation $conversation, Thread $last_thread)
public function __construct(Conversation $conversation, $last_thread)
{
$this->conversation = $conversation;
$this->last_thread = $last_thread;

111
app/Events/RealtimeChat.php Normal file
View File

@@ -0,0 +1,111 @@
<?php
/**
* Fefresh chats list when new thread created in mailbox.
*/
namespace App\Events;
use App\Conversation;
use App\Mailbox;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
use Illuminate\Queue\SerializesModels;
class RealtimeChat implements ShouldBroadcastNow
{
use SerializesModels;
/**
* The notification data.
*
* @var array
*/
public $data = [];
/**
* Create a new event instance.
*
* @return void
*/
public function __construct($data)
{
$this->data = $data;
}
/**
* Get the channels the event should broadcast on.
*
* @return Channel|array
*/
public function broadcastOn()
{
return new \Illuminate\Broadcasting\Channel($this->channelName());
}
/**
* Get the data to broadcast.
*
* @return array
*/
public function broadcastWith()
{
return $this->data;
}
/**
* Get the broadcast channel name for the event.
*
* @return string
*/
protected function channelName()
{
if (!empty($this->data['mailbox_id'])) {
return 'chat.'.$this->data['mailbox_id'];
} else {
return 'chat.0';
}
}
/**
* Helper funciton.
*/
public static function dispatchSelf($mailbox_id, $thread_id, $is_chat)
{
if (!\Helper::isChatModeAvailable()) {
return;
}
$notification_data = [
'mailbox_id' => $mailbox_id,
'thread_id' => $thread_id,
'is_chat' => (int)$is_chat,
];
event(new \App\Events\RealtimeChat($notification_data));
}
public static function processPayload($payload)
{
$user = auth()->user();
$mailbox = Mailbox::rememberForever()->find($payload->mailbox_id);
// Check if user can listen to this event.
if (!$user || !$mailbox || !$user->can('viewCached', $mailbox)) {
return [];
}
// Chats are retrieved in the template.
$template_data = [
'mailbox' => $mailbox,
];
$payload->chats_html = \View::make('mailboxes/partials/chat_list')->with($template_data)->render();
// Audio notification for chats.
if ((int)$payload->is_chat && $payload->thread_id) {
$payload->audio = [
'thread_id' => $payload->thread_id,
];
}
return $payload;
}
}

View File

@@ -88,30 +88,48 @@ class RealtimeConvNewThread implements ShouldBroadcastNow
public static function processPayload($payload)
{
$user = auth()->user();
$mailbox = Mailbox::rememberForever()->find($payload->mailbox_id);
// Check if user can listen to this event.
if (!$user || !$mailbox || !$user->can('viewCached', $mailbox)) {
if (!$user) {
return [];
}
$thread = Thread::find($payload->thread_id);
// Check if user can listen to this event.
// https://github.com/freescout-help-desk/freescout/security/advisories/GHSA-w668-wq26-6c94
if (!$thread) {
return $payload;
return [];
}
$conversation = $thread->conversation;
if (!$conversation) {
return [];
}
if (!$user->can('view', $conversation)) {
return [];
}
// Add thread html to the payload.
$template_data = [
'conversation' => $thread->conversation,
'mailbox' => $thread->conversation->mailbox,
'conversation' => $conversation,
'mailbox' => $conversation->mailbox,
'threads' => [$thread],
];
$payload->thread_html = \View::make('conversations/partials/threads')->with($template_data)->render();
$payload->conversation_user_id = $thread->conversation->user_id;
$payload->conversation_status = $thread->conversation->status;
$payload->conversation_status_class = Conversation::$status_classes[$thread->conversation->status];
$payload->conversation_status_icon = Conversation::$status_icons[$thread->conversation->status];
$payload->conversation_user_id = $conversation->user_id;
$payload->conversation_status = $conversation->status;
$payload->conversation_status_class = Conversation::$status_classes[$conversation->status];
$payload->conversation_status_icon = Conversation::$status_icons[$conversation->status];
// Audio notification for chats.
if ($conversation->isChat()) {
$payload->audio = [
'thread_id' => $thread->id,
];
}
return $payload;
}

View File

@@ -2,6 +2,7 @@
namespace App\Events;
use App\Conversation;
use Illuminate\Broadcasting\Channel;
//use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
@@ -110,4 +111,18 @@ class RealtimeConvView implements ShouldBroadcastNow
];
event(new \App\Events\RealtimeConvView($notification_data));
}
// Check access.
public static function processPayload($payload)
{
$user = auth()->user();
$conversation = Conversation::find($payload->conversation_id);
// Check if user can listen to this event.
if (!$user || !$conversation || !$user->can('view', $conversation)) {
return [];
}
return $payload;
}
}

View File

@@ -4,6 +4,7 @@
*/
namespace App\Events;
use App\Conversation;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
@@ -59,4 +60,22 @@ class RealtimeConvViewFinish implements ShouldBroadcastNow
{
return 'conv';
}
// Check access.
// $notification_data = [
// 'conversation_id' => $conversation_id,
// 'user_id' => $user_id,
// ];
public static function processPayload($payload)
{
$user = auth()->user();
$conversation = Conversation::find($payload->conversation_id);
// Check if user can listen to this event.
if (!$user || !$conversation || !$user->can('view', $conversation)) {
return [];
}
return $payload;
}
}

View File

@@ -70,10 +70,12 @@ class RealtimeMailboxNewThread implements ShouldBroadcastNow
/**
* Helper funciton.
*/
public static function dispatchSelf($mailbox_id)
public static function dispatchSelf($mailbox_id, $thread_id, $is_chat)
{
$notification_data = [
'mailbox_id' => $mailbox_id
'mailbox_id' => $mailbox_id,
'thread_id' => $thread_id,
'is_chat' => (int)$is_chat,
];
event(new \App\Events\RealtimeMailboxNewThread($notification_data));
}
@@ -105,6 +107,13 @@ class RealtimeMailboxNewThread implements ShouldBroadcastNow
$payload->folders_html = \View::make('mailboxes/partials/folders')->with($template_data)->render();
// Audio notification for chats.
if ((int)$payload->is_chat && $payload->thread_id) {
$payload->audio = [
'thread_id' => $payload->thread_id,
];
}
return $payload;
}
}

View File

@@ -15,7 +15,7 @@ class UserCreatedConversation
*
* @return void
*/
public function __construct(Conversation $conversation, Thread $last_thread)
public function __construct(Conversation $conversation, $last_thread)
{
$this->conversation = $conversation;
$this->last_thread = $last_thread;

View File

@@ -0,0 +1,28 @@
<?php
namespace App\Events;
use App\Conversation;
use App\Mailbox;
use App\Thread;
class UserMovedConversation
{
public $conversation;
//public $last_thread;
public $caused_by_user_id;
public $from_mailbox;
/**
* Create a new event instance.
*
* @return void
*/
public function __construct(Conversation $conversation, /*$last_thread,*/ $caused_by_user_id, Mailbox $from_mailbox)
{
$this->conversation = $conversation;
//$this->last_thread = $last_thread;
$this->caused_by_user_id = $caused_by_user_id;
$this->from_mailbox = $from_mailbox;
}
}

View File

@@ -10,4 +10,27 @@ class FailedJob extends Model
* Automatically converted into Carbon dates.
*/
protected $dates = ['failed_at'];
public $payload_decoded = null;
public function getPayloadDecoded()
{
if ($this->payload_decoded !== null) {
return $this->payload_decoded;
}
$this->payload_decoded = json_decode($this->payload, true);
return $this->payload_decoded;
}
public function getCommand()
{
return \App\Job::getPayloadCommand($this->getPayloadDecoded());
}
public static function retry($job_id)
{
\Artisan::call('queue:retry', ['id' => $job_id]);
}
}

View File

@@ -179,7 +179,7 @@ class Folder extends Model
$sorting = Conversation::getConvTableSorting();
if ($sorting['sort_by'] == 'date') {
if ($sorting['order'] != 'desc') {
foreach ($order_by as $block_i => $block) {
foreach ($order_by as $block_i => $block) {
foreach ($block as $field => $order) {
if ($field == 'status') {
unset($order_by[$block_i][$field]);
@@ -224,7 +224,9 @@ class Folder extends Model
public function updateCounters()
{
if (config('app.update_folder_counters_in_background')) {
\App\Jobs\UpdateFolderCounters::dispatch($this);
if (!\Illuminate\Support\Facades\Cache::has("folder_update_lock_{$this->id}")) {
\App\Jobs\UpdateFolderCounters::dispatch($this);
}
} else {
$this->updateCountersNow();
}
@@ -380,13 +382,13 @@ class Folder extends Model
} elseif ($this->type == \App\Folder::TYPE_DELETED) {
return 'user_updated_at';
} else {
return'last_reply_at';
return 'last_reply_at';
}
}
public function url($mailbox_id)
{
return \Eventy::filter('folder.url', route('mailboxes.view.folder', ['id'=>$mailbox_id, 'folder_id'=>$this->id]), $mailbox_id, $this);
return \Eventy::filter('folder.url', route('mailboxes.view.folder', ['id' => $mailbox_id, 'folder_id' => $this->id]), $mailbox_id, $this);
}
public static function create($data, $unique_per_user = true, $save = true)
@@ -394,7 +396,7 @@ class Folder extends Model
if (!isset($data['mailbox_id']) || !isset($data['type'])) {
return null;
}
$folder = new Folder ();
$folder = new Folder();
$folder->mailbox_id = $data['mailbox_id'];
$folder->type = $data['type'];

View File

@@ -4,6 +4,11 @@ namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\SendsPasswordResetEmails;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Password;
use Illuminate\Cache\RateLimiter;
use Illuminate\Validation\ValidationException;
use Illuminate\Auth\Events\Lockout;
class ForgotPasswordController extends Controller
{
@@ -20,6 +25,12 @@ class ForgotPasswordController extends Controller
use SendsPasswordResetEmails;
// Max reset password attempts per period.
const THROTTLE_ATTEMPTS = 5;
// Minutes
const THROTTLE_PERIOD = 1;
/**
* Create a new controller instance.
*
@@ -29,4 +40,73 @@ class ForgotPasswordController extends Controller
{
$this->middleware('guest');
}
/**
* Send a reset link to the given user.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Http\JsonResponse
*/
public function sendResetLinkEmail(Request $request)
{
$this->validateEmail($request);
// https://github.com/freescout-help-desk/freescout/security/advisories/GHSA-jvmv-2qcp-7855
if ($this->hasTooManyResetEmailAttempts($request)) {
event(new Lockout($request));
return $this->sendLockoutResponse($request);
}
// We will send the password reset link to this user. Once we have attempted
// to send the link, we will examine the response then see the message we
// need to show to the user. Finally, we'll send out a proper response.
$response = $this->broker()->sendResetLink(
$request->only('email')
);
if ($response == Password::RESET_LINK_SENT) {
$this->clearResetEmailAttempts($request);
} else {
$this->incrementResetEmailAttempts($request);
}
// For security purposes always return an identical response regardless of whether the email exists.
// return $response == Password::RESET_LINK_SENT
// ? $this->sendResetLinkResponse($response)
// : $this->sendResetLinkFailedResponse($request, $response);
return $this->sendResetLinkResponse(__('If an account exists for this email, you will receive a password reset link.'));
}
protected function hasTooManyResetEmailAttempts(Request $request)
{
return app(RateLimiter::class)->tooManyAttempts(
$this->throttleKey($request),
self::THROTTLE_ATTEMPTS
);
}
protected function incrementResetEmailAttempts(Request $request)
{
app(RateLimiter::class)->hit(
$this->throttleKey($request),
self::THROTTLE_PERIOD
);
}
protected function clearResetEmailAttempts(Request $request)
{
app(RateLimiter::class)->clear($this->throttleKey($request));
}
protected function throttleKey(Request $request)
{
return strtolower($request->ip()) . '|reset_email_password';
}
protected function sendLockoutResponse(Request $request)
{
throw ValidationException::withMessages([
'email' => [trans('auth.throttle', ['seconds' => self::THROTTLE_PERIOD*60])],
])->status(423);
}
}

View File

@@ -2,8 +2,10 @@
namespace App\Http\Controllers\Auth;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Illuminate\Validation\ValidationException;
class LoginController extends Controller
{
@@ -38,4 +40,40 @@ class LoginController extends Controller
{
$this->middleware('guest')->except('logout');
}
/**
* Handle a login request to the application.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Http\Response|\Illuminate\Http\JsonResponse
*/
public function login(Request $request)
{
$this->validateLogin($request);
// If the class is using the ThrottlesLogins trait, we can automatically throttle
// the login attempts for this application. We'll key this by the username and
// the IP address of the client making these requests into this application.
if ($this->hasTooManyLoginAttempts($request)) {
$this->fireLockoutEvent($request);
return $this->sendLockoutResponse($request);
}
$custom_errors = \Eventy::filter('login.custom_check', [], $request);
if ($custom_errors) {
throw ValidationException::withMessages($custom_errors);
}
if ($this->attemptLogin($request)) {
return $this->sendLoginResponse($request);
}
// If the login attempt was unsuccessful we will increment the number of attempts
// to login and redirect the user back to the login form. Of course, when this
// user surpasses their maximum number of attempts they will get locked out.
$this->incrementLoginAttempts($request);
return $this->sendFailedLoginResponse($request);
}
}

View File

@@ -7,6 +7,11 @@ use Illuminate\Foundation\Auth\ResetsPasswords;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Hash;
use Illuminate\Auth\Events\PasswordReset;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Password;
use Illuminate\Cache\RateLimiter;
use Illuminate\Validation\ValidationException;
use Illuminate\Auth\Events\Lockout;
class ResetPasswordController extends Controller
{
@@ -23,6 +28,12 @@ class ResetPasswordController extends Controller
use ResetsPasswords;
// Max reset password attempts per period.
const THROTTLE_ATTEMPTS = 5;
// Minutes
const THROTTLE_PERIOD = 1;
/**
* Where to redirect users after resetting their password.
*
@@ -40,6 +51,46 @@ class ResetPasswordController extends Controller
$this->middleware('guest');
}
/**
* Reset the given user's password.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Http\JsonResponse
*/
public function reset(Request $request)
{
$this->validate($request, $this->rules(), $this->validationErrorMessages());
if ($this->hasTooManyResetAttempts($request)) {
event(new Lockout($request));
return $this->sendLockoutResponse($request);
}
// Here we will attempt to reset the user's password. If it is successful we
// will update the password on an actual user model and persist it to the
// database. Otherwise we will parse the error and return the response.
$response = $this->broker()->reset(
$this->credentials($request), function ($user, $password) {
$this->resetPassword($user, $password);
}
);
if ($response == Password::PASSWORD_RESET) {
$this->clearResetAttempts($request);
} else {
$this->incrementResetAttempts($request);
// When user not found and token is invalid return same error for security reasons.
$response = Password::INVALID_USER;
}
// If the password was successfully reset, we will redirect the user back to
// the application's home authenticated view. If there is an error we can
// redirect them back to where they came from with their error message.
return $response == Password::PASSWORD_RESET
? $this->sendResetResponse($response)
: $this->sendResetFailedResponse($request, $response);
}
/**
* Reset the given user's password.
*
@@ -59,4 +110,37 @@ class ResetPasswordController extends Controller
//$this->guard()->login($user);
}
protected function hasTooManyResetAttempts(Request $request)
{
return app(RateLimiter::class)->tooManyAttempts(
$this->throttleKey($request),
self::THROTTLE_ATTEMPTS
);
}
protected function incrementResetAttempts(Request $request)
{
app(RateLimiter::class)->hit(
$this->throttleKey($request),
self::THROTTLE_PERIOD
);
}
protected function clearResetAttempts(Request $request)
{
app(RateLimiter::class)->clear($this->throttleKey($request));
}
protected function throttleKey(Request $request)
{
return strtolower($request->ip()) . '|reset_password';
}
protected function sendLockoutResponse(Request $request)
{
throw ValidationException::withMessages([
'email' => [trans('auth.throttle', ['seconds' => self::THROTTLE_PERIOD*60])],
])->status(423);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -23,10 +23,12 @@ class CustomersController extends Controller
/**
* Edit customer.
*/
public function update($id)
public function update(Request $request, $id)
{
$customer = Customer::findOrFail($id);
$this->checkLimitVisibility($customer);
$customer_emails = $customer->emails;
if (count($customer_emails)) {
foreach ($customer_emails as $row) {
@@ -56,6 +58,8 @@ class CustomersController extends Controller
$customer = Customer::findOrFail($id);
$flash_message = '';
$this->checkLimitVisibility($customer);
// First name or email must be specified
$validator = Validator::make($request->all(), [
'first_name' => 'nullable|string|max:255|required_without:emails.0',
@@ -77,7 +81,7 @@ class CustomersController extends Controller
// Photo
$validator->after(function ($validator) use ($customer, $request) {
if ($request->hasFile('photo_url')) {
$path_url = $customer->savePhoto($request->file('photo_url')->getRealPath(), $request->file('photo_url')->getMimeType());
$path_url = $customer->savePhoto($request->file('photo_url')->getRealPath() ?: $request->file('photo_url')->getPathname(), $request->file('photo_url')->getMimeType());
if ($path_url) {
$customer->photo_url = $path_url;
@@ -109,6 +113,17 @@ class CustomersController extends Controller
foreach ($new_emails as $new_email) {
$email = Email::where('email', $new_email)->first();
if ($email && $email->customer) {
// Prevent pulling in an out-of-scope customer by attaching one
// of that customer's emails to the current profile.
if ($email->customer_id !== $customer->id) {
if (!$this->checkLimitVisibility($email->customer, true)) {
$validator->errors()->add('email', __('The specified email belongs to a customer from an inaccessible mailbox.'));
return redirect()->route('customers.update', ['id' => $id])
->withErrors($validator)
->withInput();
}
}
// If customer whose email is removed does not have first name and other emails
// we have to create first name for this customer
if (!$email->customer->first_name && count($email->customer->emails) == 1) {
@@ -126,8 +141,8 @@ class CustomersController extends Controller
'email' => $email->email,
'tag_email_begin' => '<strong>',
'tag_email_end' => '</strong>',
'customer' => $email->customer->getFullName(),
'a_begin' => '<strong><a href="'.$email->customer->url().'" target="_blank">',
'customer' => htmlspecialchars($email->customer->getFullName()),
'a_begin' => '<strong><a href="'.$email->customer->url().'">',
'a_end' => '</a></strong>',
]).' ';
@@ -147,6 +162,15 @@ class CustomersController extends Controller
if (isset($request_data['photo_url'])) {
unset($request_data['photo_url']);
}
$nonfillable_fields = [
'channel',
'channel_id',
];
foreach ($nonfillable_fields as $field) {
if (isset($request_data[$field])) {
unset($request_data[$field]);
}
}
$customer->setData($request_data);
// Websites
@@ -193,35 +217,19 @@ class CustomersController extends Controller
return redirect()->route('customers.update', ['id' => $id]);
}
/**
* User mailboxes.
*/
public function permissions($id)
public function checkLimitVisibility($customer, $return_result = false)
{
$user = User::findOrFail($id);
$this->authorize('update', $user);
$user = auth()->user();
if (!$user->can('view', $customer)) {
if (!$return_result) {
\Helper::denyAccess();
} else {
return false;
}
}
$mailboxes = Mailbox::all();
return view('users/permissions', ['user' => $user, 'mailboxes' => $mailboxes, 'user_mailboxes' => $user->mailboxes]);
}
/**
* Save user permissions.
*
* @param int $id
* @param \Illuminate\Http\Request $request
*/
public function permissionsSave($id, Request $request)
{
$user = User::findOrFail($id);
$this->authorize('update', $user);
$user->mailboxes()->sync($request->mailboxes);
\Session::flash('flash_success', __('Permissions saved successfully'));
return redirect()->route('users.permissions', ['id' => $id]);
return true;
}
/**
@@ -233,11 +241,20 @@ class CustomersController extends Controller
{
$customer = Customer::findOrFail($id);
$conversations = $customer->conversations()
$this->checkLimitVisibility($customer);
$query = $customer->conversations()
->where('customer_id', $customer->id)
->whereIn('mailbox_id', auth()->user()->mailboxesIdsCanView())
->orderBy('created_at', 'desc')
->paginate(Conversation::DEFAULT_LIST_SIZE);
->orderBy('created_at', 'desc');
$user = auth()->user();
if ($user->canSeeOnlyAssignedConversations()) {
$query->where('user_id', '=', $user->id);
}
$conversations = $query->paginate(Conversation::DEFAULT_LIST_SIZE);
return view('customers/conversations', [
'customer' => $customer,
@@ -257,6 +274,9 @@ class CustomersController extends Controller
$q = $request->q;
$user = auth()->user();
$limited_visibility = config('app.limit_user_customer_visibility') && !$user->isAdmin();
$join_emails = false;
if ($request->search_by == 'all' || $request->search_by == 'email' || $request->exclude_email) {
$join_emails = true;
@@ -264,7 +284,12 @@ class CustomersController extends Controller
$select_list = ['customers.id', 'first_name', 'last_name'];
if ($join_emails) {
$select_list[] = 'emails.email';
if ($limited_visibility) {
// https://github.com/freescout-help-desk/freescout/issues/5032
$select_list[] = \DB::raw('MAX('.\DB::getTablePrefix().'emails.email)');
} else {
$select_list[] = 'emails.email';
}
}
if ($request->show_fields == 'phone') {
$select_list[] = 'phones';
@@ -279,18 +304,36 @@ class CustomersController extends Controller
}
}
if ($request->search_by == 'all' || $request->search_by == 'email') {
$customers_query->where('emails.email', 'like', '%'.$q.'%');
}
if ($request->exclude_email) {
$customers_query->where('emails.email', '<>', $request->exclude_email);
}
if ($request->search_by == 'all' || $request->search_by == 'name') {
$customers_query->orWhere('first_name', 'like', '%'.$q.'%')
->orWhere('last_name', 'like', '%'.$q.'%');
}
if ($request->search_by == 'phone') {
$customers_query->where('customers.phones', 'like', '%'.$q.'%');
$customers_query->where(function ($query) use ($q, $request) {
if ($request->search_by == 'all' || $request->search_by == 'email') {
$query->where('emails.email', 'like', '%'.$q.'%');
}
if ($request->exclude_email) {
$query->where('emails.email', '<>', $request->exclude_email);
}
if ($request->exclude_id) {
$query->where('customers.id', '<>', $request->exclude_id);
}
if ($request->search_by == 'all' || $request->search_by == 'name') {
$query->orWhere('first_name', 'like', '%'.$q.'%')
->orWhere('last_name', 'like', '%'.$q.'%')
->orWhere(\Helper::isPgSql() ? \DB::raw('(first_name || \' \' || last_name)') : \DB::raw('CONCAT(first_name, " ", last_name)'), 'like', '%'.$q.'%');
}
if ($request->search_by == 'phone') {
$phone_numeric = \Helper::phoneToNumeric($q);
if (!$phone_numeric) {
$phone_numeric = $q;
}
$query->where('customers.phones', 'like', '%'.$phone_numeric.'%');
}
});
if ($limited_visibility) {
$mailbox_ids = $user->mailboxesIdsCanView();
$customers_query->join('conversations', 'conversations.customer_id', '=', 'customers.id');
$customers_query->whereIn('conversations.mailbox_id', $mailbox_ids);
$customers_query->groupby('customers.id');
}
$customers = $customers_query->paginate(20);
@@ -311,7 +354,8 @@ class CustomersController extends Controller
// Get phone which matches
$phones = $customer->getPhones();
foreach ($phones as $phone) {
if (strstr($phone['value'], $q)) {
$phone_numeric = \Helper::phoneToNumeric($q);
if (strstr($phone['value'], $q) || strstr($phone['n'] ?? '', $phone_numeric)) {
$text = $phone['value'];
if ($customer->getFullName()) {
$text .= ' — '.$customer->getFullName();
@@ -333,7 +377,8 @@ class CustomersController extends Controller
if (!empty($request->use_id)) {
$id = $customer->id;
} else {
$id = $customer->email;
// https://github.com/freescout-helpdesk/freescout/issues/4057
$id = $customer->email ?? $customer->getMainEmail();
}
}
$response['results'][] = [
@@ -361,27 +406,46 @@ class CustomersController extends Controller
switch ($request->action) {
// Change conversation user
// Change conversation customer.
case 'create':
// First name or email must be specified
$validator = Validator::make($request->all(), [
$validator_config = [
'first_name' => 'required|string|max:255',
'last_name' => 'nullable|string|max:255',
'email' => 'required|email|unique:emails,email',
]);
];
$limited_visibility = config('app.limit_user_customer_visibility') && !$user->isAdmin();
if ($limited_visibility) {
$validator_config['email'] = 'required|email';
}
// First name or email must be specified.
$validator = Validator::make($request->all(), $validator_config);
if ($validator->fails()) {
foreach ($validator->errors()->getMessages()as $errors) {
foreach ($validator->errors()->getMessages() as $errors) {
foreach ($errors as $field => $message) {
$response['msg'] .= $message.' ';
}
}
}
if (!$response['msg'] && $limited_visibility) {
$existing_email = Email::where('email', $request->email)->first();
if ($existing_email
&& $existing_email->customer
&& !$user->can('view', $existing_email->customer)
) {
$response['msg'] .= __('The specified email belongs to a customer from an inaccessible mailbox.');
}
}
if (!$response['msg']) {
$customer = Customer::create($request->email, $request->all());
if ($customer) {
session()->put('user_created_customer', $customer->id);
$response['email'] = $request->email;
$response['status'] = 'success';
}
@@ -411,4 +475,48 @@ class CustomersController extends Controller
return \Response::json($response);
}
}
public function merge(Request $request, $id)
{
$customer = Customer::findOrFail($id);
$this->checkLimitVisibility($customer);
// $customers = Customer::where('id', '!=', $id)
// ->orderBy('first_name')
// ->orderBy('last_name')
// ->get();
return view('customers/merge', ['customer' => $customer]);
}
/**
* Merge handling function.
*/
public function mergeSave(Request $request, $id)
{
$request->validate([
'customer2_id' => 'required|exists:customers,id|not_in:'.$id,
//'keep_attributes' => 'array'
], [], [
'customer2_id' => __('Merge With'),
]);
$customer = Customer::findOrFail($id);
$customer2 = Customer::findOrFail($request->customer2_id);
$this->checkLimitVisibility($customer);
$this->checkLimitVisibility($customer2);
// Ensure customers are different
if ($id === $customer2->id) {
return redirect()->back()->with('error', __('Cannot merge the same customer'));
}
$customer->mergeWith($customer2/*, $request->keep_attributes ?? []*/);
\Session::flash('flash_success_floating', __('Customers merged successfully'));
return redirect()->route('customers.update', ['id' => $id]);
}
}

View File

@@ -5,6 +5,7 @@ namespace App\Http\Controllers;
use App\Conversation;
use App\Folder;
use App\Mailbox;
use App\MailboxUser;
use App\Thread;
use App\User;
use Illuminate\Http\Request;
@@ -35,7 +36,7 @@ class MailboxesController extends Controller
if (!\Eventy::filter('user.can_view_mailbox_menu', false, $user)) {
foreach ($mailboxes as $i => $mailbox) {
if (!$user->canManageMailbox($mailbox->id)) {
if (!$user->canManageMailbox($mailbox)) {
$mailboxes->forget($i);
}
}
@@ -85,12 +86,18 @@ class MailboxesController extends Controller
->withInput();
}
$request_data = [
'email' => $request->email,
'name' => trim(strip_tags($request->name)),
'ratings' => $request->ratings ?? 1,
];
$mailbox = new Mailbox();
$mailbox->fill($request->all());
$mailbox->fill($request_data);
$mailbox->save();
$mailbox->users()->sync($request->users);
$mailbox->users()->sync($request->users ?: []);
$mailbox->syncPersonalFolders($request->users);
\Session::flash('flash_success_floating', __('Mailbox created successfully'));
@@ -105,6 +112,8 @@ class MailboxesController extends Controller
{
$mailbox = Mailbox::findOrFail($id);
$user = auth()->user();
// Redirect to the accessible page.
if (!$user->can('updateSettings', $mailbox) && !$user->can('updateEmailSignature', $mailbox)) {
$accessible_route = '';
@@ -159,30 +168,35 @@ class MailboxesController extends Controller
$mailbox = Mailbox::findOrFail($id);
$user = auth()->user();
$can_update_settings = $user->can('updateSettings', $mailbox);
$can_update_signature = $user->can('updateEmailSignature', $mailbox);
if (!$user->can('updateSettings', $mailbox) && !$user->can('updateEmailSignature', $mailbox)) {
if (!$can_update_settings && !$can_update_signature) {
\Helper::denyAccess();
}
// Checkboxes
$request->merge([
'aliases_reply' => ($request->filled('aliases_reply') ?? false),
]);
$allowed_fields = [];
if ($user->can('updateSettings', $mailbox)) {
if ($can_update_settings) {
// Checkboxes
$request->merge([
'aliases_reply' => ($request->filled('aliases_reply') ?? false),
]);
// if not admin, the text only fields don't pass so spike them into the request.
if (!auth()->user()->isAdmin()) {
if (!$user->isAdmin()) {
$request->merge([
'name' => $mailbox->name,
'email' => $mailbox->email
'email' => $mailbox->email,
]);
}
$validator = Validator::make($request->all(), [
'name' => 'required|string|max:40',
'email' => 'required|string|email|max:128|unique:mailboxes,email,'.$id,
'aliases' => 'nullable|string|max:255',
'aliases' => 'nullable|string',
'from_name' => 'required|integer',
'from_name_custom' => 'nullable|string|max:128',
'ticket_status' => 'required|integer',
@@ -203,9 +217,22 @@ class MailboxesController extends Controller
->withErrors($validator)
->withInput();
}
$allowed_fields = [
'name',
'email',
'aliases',
'aliases_reply',
'auto_bcc',
'from_name',
'from_name_custom',
'ticket_status',
'ticket_assignee',
'before_reply',
];
}
if ($user->can('updateEmailSignature', $mailbox)) {
if ($can_update_signature) {
$validator = Validator::make($request->all(), [
'signature' => 'nullable|string',
]);
@@ -215,11 +242,42 @@ class MailboxesController extends Controller
->withErrors($validator)
->withInput();
}
$allowed_fields[] = 'signature';
}
\Eventy::action('mailbox.settings_before_save', $mailbox, $request);
$mailbox->fill($request->all());
// Leave only allowed fields.
$fields = $request->all();
foreach ($fields as $field_name => $value) {
if (!in_array($field_name, $allowed_fields)) {
unset($fields[$field_name]);
}
}
$mailbox->fill($fields);
// Save state.
if ($user->isAdmin()) {
if ($request->filled('state')) {
$mailbox->state = Mailbox::STATE_ARCHIVED;
} else {
$mailbox->state = Mailbox::STATE_ACTIVE;
}
}
// Chat: Start a new conversation when receiving a reply to the closed / deleted Chat conversation.
if ($can_update_settings) {
if (!empty($request->chat_start_new)) {
$mailbox->setMetaParam('chat_start_new', true);
} else {
$mailbox->removeMetaParam('chat_start_new');
}
}
// Purify signature HTML to avoid sending unsafe HTML to customers by email.
//$mailbox->signature = \Helper::stripDangerousTags($mailbox->signature);
$mailbox->signature = Mailbox::sanitizeSignature($mailbox->signature);
$mailbox->save();
@@ -268,8 +326,24 @@ class MailboxesController extends Controller
$this->authorize('updatePermissions', $mailbox);
$user = auth()->user();
$user_ids = \Eventy::filter('mailbox.permission_users', $request->users ?? [], $id);
// Get pivot data from mailbox_user to pass to sync() not to loose values.
// https://github.com/freescout-help-desk/freescout/issues/4339
$users_with_settings = [];
$mailbox_users = MailboxUser::where('mailbox_id', $id)
//->whereIn('user_id', $user_ids)
->get()
->keyBy('user_id')
->toArray();
foreach ($user_ids as $user_id) {
$users_with_settings[$user_id] = $mailbox_users[$user_id] ?? [];
}
// Settings for admins are being reset here.
// So we restore them below.
$mailbox->users()->sync($users_with_settings);
$mailbox->users()->sync(\Eventy::filter('mailbox.permission_users', $request->users, $id));
$mailbox->syncPersonalFolders($request->users);
// Save admins settings.
@@ -281,6 +355,15 @@ class MailboxesController extends Controller
$admin->mailboxes()->attach($id);
$mailbox_user = $admin->mailboxesWithSettings()->where('mailbox_id', $id)->first();
}
// Restore settings for admins.
if (!empty($mailbox_users[$admin->id])) {
$admin_settings = $mailbox_users[$admin->id];
foreach (MailboxUser::$pivot_settings as $pivot_parameter) {
if (isset($admin_settings[$pivot_parameter])) {
$mailbox_user->settings->$pivot_parameter = $admin_settings[$pivot_parameter];
}
}
}
$mailbox_user->settings->hide = (isset($request->managers[$admin->id]['hide']) ? (int)$request->managers[$admin->id]['hide'] : false);
$mailbox_user->settings->save();
}
@@ -332,9 +415,9 @@ class MailboxesController extends Controller
if ($request->out_method == Mailbox::OUT_METHOD_SMTP) {
$validator = Validator::make($request->all(), [
'out_server' => 'required|string|max:255',
'out_server' => 'required|string|max:255|safehost',
'out_port' => 'required|integer',
'out_username' => 'nullable|string|max:100',
'out_username' => 'nullable|string|max:255',
'out_password' => 'nullable|string|max:255',
'out_encryption' => 'required|integer',
]);
@@ -352,6 +435,18 @@ class MailboxesController extends Controller
} else {
$params = $request->all();
}
// Leave only allowed fields.
$params = \Helper::filterArrayByKeys($params, [
'out_method',
'out_server',
'out_port',
'out_username',
'out_password',
'out_encryption',
'send_test_to',
]);
$mailbox->fill($params);
$mailbox->save();
@@ -383,13 +478,22 @@ class MailboxesController extends Controller
];
$validator = Validator::make($fields, [
'in_server' => 'required',
//'in_server' => 'required',
'in_port' => 'required',
'in_username' => 'required',
'in_password' => 'required',
]);
return view('mailboxes/connection_incoming', ['mailbox' => $mailbox, 'flashes' => $this->mailboxActiveWarning($mailbox)])->withErrors($validator);
$response = view('mailboxes/connection_incoming', ['mailbox' => $mailbox, 'flashes' => $this->mailboxActiveWarning($mailbox)]);
if (empty($request->session()->get('errors'))) {
if (empty($mailbox->in_server)) {
$validator->errors()->add('in_server', 'dummy');
}
$response->withErrors($validator);
}
return $response;
}
/**
@@ -400,18 +504,18 @@ class MailboxesController extends Controller
$mailbox = Mailbox::findOrFail($id);
$this->authorize('admin', $mailbox);
// $validator = Validator::make($request->all(), [
// 'in_server' => 'nullable|string|max:255',
// 'in_port' => 'nullable|integer',
// 'in_username' => 'nullable|string|max:100',
// 'in_password' => 'nullable|string|max:255',
// ]);
$validator = Validator::make($request->all(), [
'in_server' => 'required|string|max:255|safehost',
'in_port' => 'required|integer',
'in_username' => 'required|string|max:100',
'in_password' => 'required|string|max:255',
]);
// if ($validator->fails()) {
// return redirect()->route('mailboxes.connection.incoming', ['id' => $id])
// ->withErrors($validator)
// ->withInput();
// }
if ($validator->fails()) {
return redirect()->route('mailboxes.connection.incoming', ['id' => $id])
->withErrors($validator)
->withInput();
}
// Checkboxes
$request->merge([
@@ -427,6 +531,19 @@ class MailboxesController extends Controller
\Eventy::action('mailbox.incoming_settings_before_save', $mailbox, $request);
// Leave only allowed fields.
$params = \Helper::filterArrayByKeys($params, [
'in_protocol',
'in_server',
'in_port',
'in_username',
'in_password',
'in_encryption',
'in_imap_folders',
'in_validate_cert',
'imap_sent_folder',
]);
$mailbox->fill($params);
// Save IMAP Folders.
@@ -449,7 +566,7 @@ class MailboxesController extends Controller
/**
* View mailbox.
*/
public function view($id, $folder_id = null)
public function view(Request $request, $id, $folder_id = null)
{
$user = auth()->user();
@@ -474,7 +591,9 @@ class MailboxesController extends Controller
$this->authorize('view', $folder);
$query_conversations = Conversation::getQueryByFolder($folder, $user->id);
$conversations = $folder->queryAddOrderBy($query_conversations)->paginate(Conversation::DEFAULT_LIST_SIZE);
$conversations = $folder->queryAddOrderBy($query_conversations)->paginate(
Conversation::DEFAULT_LIST_SIZE, ['*'], 'page', $request->get('page')
);
return view('mailboxes/view', [
'mailbox' => $mailbox,
@@ -492,14 +611,14 @@ class MailboxesController extends Controller
if (Route::currentRouteName() != 'mailboxes.connection' && !$mailbox->isOutActive()) {
$flashes[] = [
'type' => 'warning',
'text' => __('Sending emails need to be configured for the mailbox in order to send emails to customers and support agents').' ('.__('Connection Settings').' » <a href="'.route('mailboxes.connection', ['id' => $mailbox->id]).'">'.__('Sending Emails').'</a>)',
'text' => '<i class="glyphicon glyphicon-flash"></i> '.__('Sending emails need to be configured for the mailbox in order to send emails to customers and support agents').' ('.__('Connection Settings').' » <a href="'.route('mailboxes.connection', ['id' => $mailbox->id]).'">'.__('Sending Emails').'</a>)',
'unescaped' => true,
];
}
if (Route::currentRouteName() != 'mailboxes.connection.incoming' && !$mailbox->isInActive()) {
$flashes[] = [
'type' => 'warning',
'text' => __('Receiving emails need to be configured for the mailbox in order to fetch emails from your support email address').' ('.__('Connection Settings').' » <a href="'.route('mailboxes.connection.incoming', ['id' => $mailbox->id]).'">'.__('Receiving Emails').'</a>)',
'text' => '<i class="glyphicon glyphicon-flash"></i> '.__('Receiving emails need to be configured for the mailbox in order to fetch emails from your support email address').' ('.__('Connection Settings').' » <a href="'.route('mailboxes.connection.incoming', ['id' => $mailbox->id]).'">'.__('Fetching Emails').'</a>)',
'unescaped' => true,
];
}
@@ -558,7 +677,16 @@ class MailboxesController extends Controller
}
}
$mailbox->fill($request->all());
$data = [
'auto_reply_enabled' => $request->auto_reply_enabled,
'auto_reply_subject' => $request->auto_reply_subject,
'auto_reply_message' => $request->auto_reply_message,
];
$mailbox->fill($data);
$mailbox->auto_reply_message = \Helper::purifyHtml($mailbox->auto_reply_message);
// To be 100% sure.
$mailbox->auto_reply_message = \Helper::stripDangerousTags($mailbox->auto_reply_message);
$mailbox->save();
@@ -613,24 +741,37 @@ class MailboxesController extends Controller
// Check if outgoing port is open.
if (!$response['msg'] && $mailbox->out_method == Mailbox::OUT_METHOD_SMTP) {
$test_result = \Helper::checkPort($mailbox->out_server, $mailbox->out_port);
if (!$test_result) {
$response['msg'] = __(':host is not available on :port port. Make sure that :host address is correct and that outgoing port :port on YOUR server is open.', ['host' => '<strong>'.$mailbox->out_server.'</strong>', 'port' => '<strong>'.$mailbox->out_port.'</strong>']);
$test_result = false;
try {
$test_result = \Helper::checkPort($mailbox->out_server, $mailbox->out_port);
} catch (\Exception $e) {
if ($e->getCode() == \Helper::EXCEPTION_UNSAFE_URL) {
$response['msg'] = $e->getMessage();
}
}
if (empty($response['msg']) && !$test_result) {
$response['msg'] = __(':host is not available on :port port. Make sure that :host address is correct and that outgoing port :port on YOUR server is open.', ['host' => $mailbox->out_server, 'port' => $mailbox->out_port]);
}
}
if (!$response['msg']) {
$test_result = false;
$test_result = [
'status' => 'error',
];
try {
$test_result = \App\Misc\Mail::sendTestMail($request->to, $mailbox);
$test_result = \MailHelper::sendTestMail($request->to, $mailbox);
} catch (\Exception $e) {
$response['msg'] = $e->getMessage();
$test_result['msg'] = $e->getMessage();
}
if (!$test_result && !$response['msg']) {
$response['msg'] = __('Error occurred sending email. Please check your mail server logs for more details.');
if ($test_result['status'] == 'error') {
$response['msg'] = $test_result['msg']
?: __('Error occurred sending email. Please check your mail server logs for more details.');
}
$response['log'] = $test_result['log'] ?? '';
}
if (!$response['msg']) {
@@ -659,22 +800,30 @@ class MailboxesController extends Controller
// Check if outgoing port is open.
if (!$response['msg'] && !$tested) {
$test_result = \Helper::checkPort($mailbox->in_server, $mailbox->in_port);
if (!$test_result) {
$response['msg'] = __(':host is not available on :port port. Make sure that :host address is correct and that outgoing port :port on YOUR server is open.', ['host' => '<strong>'.$mailbox->in_server.'</strong>', 'port' => '<strong>'.$mailbox->in_port.'</strong>']);
$test_result = false;
try {
$test_result = \Helper::checkPort($mailbox->in_server, $mailbox->in_port);
} catch (\Exception $e) {
if ($e->getCode() == \Helper::EXCEPTION_UNSAFE_URL) {
$response['msg'] = $e->getMessage();
}
}
if (empty($response['msg']) && !$test_result) {
$response['msg'] = __(':host is not available on :port port. Make sure that :host address is correct and that outgoing port :port on YOUR server is open.', ['host' => $mailbox->in_server, 'port' => $mailbox->in_port]);
}
}
if (!$response['msg'] && !$tested) {
$test_result = false;
try {
$test_result = \MailHelper::fetchTest($mailbox);
} catch (\Exception $e) {
$response['msg'] = $e->getMessage();
}
$test_result = \MailHelper::fetchTest($mailbox);
if (!$test_result && !$response['msg']) {
$response['log'] = $test_result['log'] ?? '';
if ($test_result['result'] != 'success' && $test_result['msg']) {
$response['msg'] = $test_result['msg'];
}
if ($test_result['result'] != 'success' && !$response['msg']) {
$response['msg'] = __('Error occurred connecting to the server');
}
}
@@ -704,28 +853,33 @@ class MailboxesController extends Controller
$imap_folders = $client->getFolders();
$response['folders'] = [];
if (count($imap_folders)) {
foreach ($imap_folders as $imap_folder) {
if (!empty($imap_folder->name)) {
$response['folders'][] = $imap_folder->name;
}
// Maybe we need a recursion here.
if (!empty($imap_folder->children)) {
foreach ($imap_folder->children as $child_imap_folder) {
// Old library.
if (!empty($child_imap_folder->fullName)) {
$response['folders'][] = $child_imap_folder->fullName;
}
// New library.
if (!empty($child_imap_folder->full_name)) {
$response['folders'][] = $child_imap_folder->full_name;
}
}
}
}
$response = $this->interateFolders($response, $imap_folders);
$response['folders'] = array_values(array_unique($response['folders']));
}
if (count($response['folders'])) {
// https://github.com/freescout-helpdesk/freescout/issues/3933
// Exclude duplicate INBOX.Name and Name folders.
// $folder_excluded = false;
// foreach ($response['folders'] as $i => $folder_name) {
// if (\Str::startsWith($folder_name, 'INBOX.')) {
// foreach ($response['folders'] as $folder_name2) {
// if ($folder_name != $folder_name2 && $folder_name == 'INBOX.'.$folder_name2) {
// unset($response['folders'][$i]);
// $folder_excluded = true;
// continue 2;
// }
// }
// }
// }
// if ($folder_excluded) {
// $response['folders'] = array_values($response['folders']);
// }
$response['msg_success'] = __('IMAP folders retrieved: '.implode(', ', $response['folders']));
} else {
$response['msg_success'] = __('Connected, but no IMAP folders found');
@@ -755,20 +909,7 @@ class MailboxesController extends Controller
if (!$response['msg']) {
// Remove threads and conversations.
$conversation_ids = $mailbox->conversations()->pluck('id')->toArray();
for ($i=0; $i < ceil(count($conversation_ids) / \Helper::IN_LIMIT); $i++) {
$slice_ids = array_slice($conversation_ids, $i*\Helper::IN_LIMIT, \Helper::IN_LIMIT);
Thread::whereIn('conversation_id', $slice_ids)->delete();
}
$mailbox->conversations()->delete();
$mailbox->users()->sync([]);
$mailbox->folders()->delete();
// Maybe remove notifications on events in this mailbox?
$mailbox->delete();
$mailbox->deleteMailbox();
\Session::flash('flash_success_floating', __('Mailbox deleted'));
@@ -778,17 +919,23 @@ class MailboxesController extends Controller
// Mute notifications
case 'mute':
if (!$user) {
\Helper::denyAccess();
}
$mailbox = Mailbox::find($request->mailbox_id);
if (!$mailbox) {
$response['msg'] = __('Mailbox not found');
} elseif (!$user->can('view', $mailbox)) {
$response['msg'] = __('Not enough permissions');
}
if (!$response['msg']) {
$mailbox_user = $user->mailboxesWithSettings()->where('mailbox_id', $mailbox->id)->first();
if (!$mailbox_user) {
// User may not be connected to the mailbox yet
if (!$mailbox_user && $user->isAdmin()) {
// Admin may not be connected to the mailbox yet
$user->mailboxes()->attach($mailbox->id);
$mailbox_user = $user->mailboxesWithSettings()->where('mailbox_id', $mailbox->id)->first();
}
@@ -811,10 +958,36 @@ class MailboxesController extends Controller
return \Response::json($response);
}
// Recursively interate over folders.
public function interateFolders($response, $imap_folders, $subfolder = false)
{
foreach ($imap_folders as $imap_folder) {
if (!empty($imap_folder->name) && !$subfolder) {
$response['folders'][] = $imap_folder->name;
}
// Check for children and recurse.
if (!empty($imap_folder->children)) {
$response = $this->interateFolders($response, $imap_folder->children, true);
}
// Old library.
if (!empty($imap_folder->fullName)) {
$response['folders'][] = $imap_folder->fullName;
} elseif (!empty($imap_folder->full_name)) {
// New library.
$response['folders'][] = $imap_folder->full_name;
}
}
return $response;
}
public function oauth(Request $request)
{
$mailbox_id = $request->id ?? '';
$provider = $request->provider ?? '';
$in_out = $request->in_out ?? 'in';
$state_data = [];
if (!empty($request->state)) {
@@ -825,6 +998,9 @@ class MailboxesController extends Controller
if (!empty($state_data['provider'])) {
$provider = $state_data['provider'];
}
if (!empty($state_data['in_out'])) {
$in_out = $state_data['in_out'];
}
}
// MS Exchange.
@@ -842,10 +1018,17 @@ class MailboxesController extends Controller
if (empty($mailbox)) {
return __('Mailbox not found').': '.$mailbox_id;
}
if (empty($mailbox->in_username)) {
if ($in_out == 'in') {
$username = $mailbox->getInOauthClientId();
$password = $mailbox->in_password;
} else {
$username = $mailbox->getOutOauthClientId();
$password = $mailbox->out_password;
}
if (empty($username)) {
return 'Enter oAuth Client ID as Username and save mailbox settings';
}
if (empty($mailbox->in_password)) {
if (empty($password)) {
return 'Enter oAuth Client Secret as Password and save mailbox settings';
}
@@ -855,14 +1038,16 @@ class MailboxesController extends Controller
}
if (empty($request->code)) {
// Start.
$state = [
'provider' => $provider,
'mailbox_id' => $mailbox_id,
'state' => crc32($mailbox->in_username.$mailbox->in_password),
'in_out' => $in_out,
'state' => crc32($username.$password),
];
$url = \MailHelper::oauthGetAuthorizationUrl(\MailHelper::OAUTH_PROVIDER_MICROSOFT, [
$url = \MailHelper::oauthGetAuthorizationUrl($provider, [
'state' => json_encode($state),
'client_id' => $mailbox->in_username,
'client_id' => $username,
]);
if ($url) {
\Session::put('mailbox_oauth_'.$provider.'_'.$mailbox_id, $state);
@@ -883,21 +1068,40 @@ class MailboxesController extends Controller
return 'Invalid oAuth state';
} else {
// state is set.
// Try to get an access token (using the authorization code grant)
$token_data = \MailHelper::oauthGetAccessToken(\MailHelper::OAUTH_PROVIDER_MICROSOFT, [
'client_id' => $mailbox->in_username,
'client_secret' => $mailbox->in_password,
$token_data = \MailHelper::oauthGetAccessToken($provider, [
'client_id' => $username,
'client_secret' => $password,
'code' => $request->code,
]);
if (!empty($token_data['a_token'])) {
// Set username and password for the oppozite in_out.
if ($in_out == 'in') {
if (empty($mailbox->out_server)
|| ($mailbox->isOutServerOauth()
&& (!$mailbox->out_username || $mailbox->out_username == $username))
) {
$mailbox->out_username = $username;
$mailbox->out_password = $password;
}
//$mailbox->out_method = Mailbox::OUT_METHOD_SMTP;
} else {
$mailbox->in_username = $username;
$mailbox->in_password = $password;
}
$mailbox->setMetaParam('oauth', $token_data, true);
} elseif (!empty($token_data['error'])) {
return __('Error occurred').': '.htmlspecialchars($token_data['error']);
}
return redirect()->route('mailboxes.connection.incoming', ['id' => $mailbox_id]);
if ($in_out == 'in') {
$route = 'mailboxes.connection.incoming';
} else {
$route = 'mailboxes.connection';
}
return redirect()->route($route, ['id' => $mailbox_id]);
}
}
@@ -905,12 +1109,24 @@ class MailboxesController extends Controller
{
$mailbox_id = $request->id ?? '';
$provider = $request->provider ?? '';
$in_out = $request->in_out ?? 'in';
$mailbox = Mailbox::findOrFail($mailbox_id);
$this->authorize('admin', $mailbox);
if (!\Helper::hashEquals(csrf_token(), $request->token)) {
throw new \Illuminate\Session\TokenMismatchException;
}
// oAuth Disconnect.
$mailbox->removeMetaParam('oauth', true);
return \MailHelper::oauthDisconnect($provider, route('mailboxes.connection.incoming', ['id' => $mailbox_id]));
if ($in_out == 'in') {
$route = 'mailboxes.connection.incoming';
} else {
$route = 'mailboxes.connection';
}
return \MailHelper::oauthDisconnect($provider, route($route, ['id' => $mailbox_id]), $mailbox);
}
}

View File

@@ -35,7 +35,11 @@ class ModulesController extends Controller
$flash = \Cache::get('modules_flash');
if ($flash) {
$flashes[] = $flash;
if (is_array($flash) && !isset($flash['text'])) {
$flashes = $flash;
} else {
$flashes[] = $flash;
}
\Cache::forget('modules_flash');
}
@@ -72,6 +76,9 @@ class ModulesController extends Controller
'installed' => true,
'activated' => \App\Module::isLicenseActivated($module->getAlias(), $module->get('authorUrl')),
'license' => \App\Module::getLicense($module->getAlias()),
// Update configuration for third party modules
'latestVersionNumberUrl' => $module->get('latestVersionUrl'),
'latestVersionZipUrl' => $module->get('latestVersionZipUrl'),
// Determined later
'new_version' => '',
];
@@ -89,17 +96,24 @@ class ModulesController extends Controller
if (is_array($modules_directory)) {
foreach ($modules_directory as $i_dir => $dir_module) {
$modules_directory[$i_dir] = \App\Module::formatModuleData($dir_module);
$dir_module = \App\Module::formatModuleData($dir_module);
$modules_directory[$i_dir] = $dir_module;
// Remove modules without aliases
if (empty($dir_module['alias'])) {
unset($modules_directory[$i_dir]);
}
$all_modules[$dir_module['alias']] = $dir_module['name'];
foreach ($installed_modules as $i_installed => $module) {
if ($dir_module['alias'] == $module['alias']) {
// Set image from director
$installed_modules[$i_installed]['img'] = $dir_module['img'];
$installed_modules[$i_installed] = \App\Module::formatModuleData($installed_modules[$i_installed]);
// Remove installed modules from modules directory.
unset($modules_directory[$i_dir]);
@@ -113,27 +127,86 @@ class ModulesController extends Controller
}
}
if (empty($dir_module['authorUrl']) || !\App\Module::isOfficial($dir_module['authorUrl'])) {
unset($modules_directory[$i_dir]);
continue;
}
if (!empty($dir_module['requiredPhpExtensions'])) {
$modules_directory[$i_dir]['requiredPhpExtensionsMissing'] = \App\Module::getMissingExtensions($dir_module['requiredPhpExtensions']);
}
$modules_directory[$i_dir]['active'] = \App\Module::isActive($dir_module['alias']);
$modules_directory[$i_dir]['activated'] = false;
// Do not show third-party modules in Modules Derectory.
// Do not show third-party modules in Modules Directory.
if (\App\Module::isThirdParty($dir_module)) {
$third_party_modules[] = $modules_directory[$i_dir];
unset($modules_directory[$i_dir]);
} elseif (empty($dir_module['authorUrl']) || !\App\Module::isOfficial($dir_module['authorUrl'])) {
unset($modules_directory[$i_dir]);
continue;
}
}
} else {
$modules_directory = [];
}
// Loop through each installed module
foreach ($installed_modules as $i_installed => $module) {
// Check if the module is an official one
if (\App\Module::isOfficial($module['authorUrl'])) {
continue;
}
// Get the URL for the latest version of the module
$latest_version_number_url = $module['latestVersionNumberUrl'] ?? null;
if (! $latest_version_number_url) {
continue;
}
// Create a new Guzzle HTTP client
$client = new \GuzzleHttp\Client();
try {
// Send a GET request to the latest version URL
$response = $client->request('GET', $latest_version_number_url, \Helper::setGuzzleDefaultOptions());
// Get the latest version number from the response body
$latest_version = trim((string) $response->getBody());
if (empty($latest_version)) {
continue;
}
// If it is the module.json file - try to parse the body.
preg_match('#"version":[^"]*"([\d\.]+)"#', $latest_version, $m);
if (!empty($m[1])) {
$latest_version = $m[1];
}
// Get the current version of the module
$current_version = $module['version'];
} catch (\Exception $e) {
// If there's an exception, skip to the next iteration
continue;
}
// If the latest version is greater than the current version
if (version_compare($latest_version, $current_version, '>')) {
// Update the installed module's version
$installed_modules[ $i_installed ]['new_version'] = $latest_version;
// Set the flag to indicate that updates are available
$updates_available = true;
}
}
// Check modules symlinks. Somestimes instead of symlinks folders with files appear.
$invalid_symlinks = \App\Module::checkSymlinks(
collect($installed_modules)->where('active', true)->pluck('alias')->toArray()
);
// Sort all modules.
asort($all_modules);
usort($third_party_modules, function ($a, $b) {
return strcasecmp($a['name'], $b['name']);
});
return view('modules/modules', [
'installed_modules' => $installed_modules,
'modules_directory' => $modules_directory,
@@ -141,6 +214,7 @@ class ModulesController extends Controller
'flashes' => $flashes,
'updates_available' => $updates_available,
'all_modules' => $all_modules,
'invalid_symlinks' => $invalid_symlinks,
]);
}
@@ -156,10 +230,11 @@ class ModulesController extends Controller
switch ($request->action) {
// Install module or activate license.
case 'install':
case 'activate_license':
$license = $request->license;
$alias = $request->alias;
$alias = preg_replace("#[^a-zA-Z0-9_\-]#", '', $request->alias);
if (!$license) {
$response['msg'] = __('Empty license key');
@@ -246,14 +321,17 @@ class ModulesController extends Controller
$response['status'] = 'success';
}
} elseif (!empty($result['error'])) {
$response['msg'] = $this->getErrorMessage($result['error'], $result);
$response['msg'] = \App\Module::getErrorMessage($result['error'], $result);
} else {
$response['msg'] = __('Error occurred. Please try again later.');
}
}
}
break;
// If everything is fine try to activate the Module.
if (!empty($response['msg'])) {
break;
}
// Continue to activation.
case 'activate':
$alias = $request->alias;
$module = \Module::findByAlias($alias);
@@ -266,7 +344,7 @@ class ModulesController extends Controller
if (!$response['msg']) {
if (!empty($module->get('authorUrl')) && $module->isOfficial()) {
$params = [
'license' => $module->getLicense(),
'license' => $request->license ?: $module->getLicense(),
'module_alias' => $alias,
'url' => \App\Module::getAppUrl(),
];
@@ -290,8 +368,9 @@ class ModulesController extends Controller
break;
case 'inactive':
$response['msg'] = __('License key has not been activated yet');
break;
case 'site_inactive':
$response['msg'] = __('Your domain is deactivated');
$response['msg'] = __('No activations left for this license key').' ('.__("Use 'Deactivate License' link above to transfer license key from another domain").')';
break;
}
} elseif (!empty($license_result['status']) && $license_result['status'] == 'inactive') {
@@ -305,18 +384,67 @@ class ModulesController extends Controller
if (!empty($result['status']) && $result['status'] == 'valid') {
// Success.
} elseif (!empty($result['error'])) {
$response['msg'] = $this->getErrorMessage($result['error'], $result);
$response['msg'] = \App\Module::getErrorMessage($result['error'], $result);
} else {
// Some unknown error. Do nothing.
}
}
}
} else {
// Check folder names for custom modules o avoid errors after activation:
// Class "Modules\CustomModule\Providers\CustomModuleServiceProvider" not found.
try {
$old_path = '';
$correct_path = '';
foreach (\Module::getScanPaths() as $key => $path) {
$manifests = \Module::getFiles()->glob("{$path}/module.json");
is_array($manifests) || $manifests = [];
// Determine correct module folder name from providers in module.json.
// Each module must have at least on "provider" specified.
foreach ($manifests as $manifest) {
$manifest_json = \Nwidart\Modules\Json::make($manifest);
if ($manifest_json->get('alias') != $alias) {
continue;
}
$providers = $manifest_json->get('providers');
$correct_folder_name = $providers[0] ?? '';
$correct_folder_name = preg_replace('#^Modules\\\\([^\\\\]+)\\\\.*#', '$1', $correct_folder_name);
if (!$correct_folder_name) {
break;
}
// Rename module's folder into correct name.
$old_path = str_replace('/module.json', '', $manifest);
$correct_path = \Module::getPath().'/'.$correct_folder_name;
if (\File::exists($old_path) && !\File::exists($correct_path)) {
\File::move($old_path, $correct_path);
// Re-scan and re-cache modules.
\Module::scan();
}
break;
}
}
} catch (\Exception $e) {
if ($old_path && $correct_path) {
$response['msg'] = __('Rename ":old_path" into ":new_path"', [
'old_path' => str_replace(\Module::getPath(), '/Modules', $old_path),
'new_path' => str_replace(\Module::getPath(), '/Modules', $correct_path),
]);
} else {
\Helper::logException($e, '[Modules] Error occured checking module folder name on activation');
}
}
}
}
if (!$response['msg']) {
\App\Module::setActive($alias, true);
$user_locale = app()->getLocale();
$outputLog = new BufferedOutput();
\Artisan::call('freescout:module-install', ['module_alias' => $alias], $outputLog);
$output = $outputLog->fetch();
@@ -327,6 +455,9 @@ class ModulesController extends Controller
$name = $module->getName();
}
// After clearing cache the locale may be not set.
\Helper::setUserLocale($user_locale);
$type = 'danger';
$msg = __('Error occurred activating ":name" module', ['name' => $name]);
if (session('flashes_floating') && is_array(session('flashes_floating'))) {
@@ -376,6 +507,8 @@ class ModulesController extends Controller
$alias = $request->alias;
\App\Module::setActive($alias, false);
$user_locale = app()->getLocale();
$outputLog = new BufferedOutput();
\Artisan::call('freescout:clear-cache', [], $outputLog);
$output = $outputLog->fetch();
@@ -387,6 +520,9 @@ class ModulesController extends Controller
$name = $module->getName();
}
// After clearing cache the locale may be not set.
\Helper::setUserLocale($user_locale);
$type = 'danger';
$msg = __('Error occurred deactivating :name module', ['name' => $name]);
if (strstr($output, 'Configuration cached successfully')) {
@@ -446,7 +582,9 @@ class ModulesController extends Controller
$response['status'] = 'success';
} elseif (!empty($result['error'])) {
$response['msg'] = $this->getErrorMessage($result['error'], $result);
$response['msg'] = \App\Module::getErrorMessage($result['error'], $result);
} elseif (!empty($result['status']) && $result['status'] == 'error') {
$response['msg'] = __('License key does not exist');
} else {
$response['msg'] = __('Error occurred. Please try again later.');
}
@@ -473,122 +611,34 @@ class ModulesController extends Controller
break;
case 'update':
$alias = $request->alias;
$module = \Module::findByAlias($alias);
$update_result = \App\Module::updateModule($request->alias);
if (!$module) {
$response['msg'] = __('Module not found').': '.$alias;
}
if ($update_result['download_error']) {
$response['reload'] = true;
// Download new version
$download_error = false;
if (!$response['msg']) {
$params = [
'license' => \App\Module::getLicense($alias),
'module_alias' => $alias,
'url' => \App\Module::getAppUrl(),
];
$license_details = WpApi::getVersion($params);
if ($update_result['msg']) {
\Session::flash('flash_error_floating', $update_result['msg']);
}
if (WpApi::$lastError) {
$response['msg'] = WpApi::$lastError['message'];
} elseif (!empty($license_details['code']) && !empty($license_details['message'])) {
$response['msg'] = $license_details['message'];
} elseif (!empty($license_details['required_app_version']) && !\Helper::checkAppVersion($license_details['required_app_version'])) {
$response['msg'] = 'Module requires app version:'.' '.$license_details['required_app_version'];
} elseif (!empty($license_details['download_link'])) {
// Download module
$module_archive = \Module::getPath().DIRECTORY_SEPARATOR.$alias.'.zip';
try {
\Helper::downloadRemoteFile($license_details['download_link'], $module_archive);
} catch (\Exception $e) {
$response['msg'] = $e->getMessage();
}
if (!file_exists($module_archive)) {
$download_error = true;
} else {
// Extract
try {
// Sometimes by some reason Public folder becomes a symlink leading to itself.
// It causes an error during updating process.
// https://github.com/freescout-helpdesk/freescout/issues/2709
$public_folder = $module->getPath().DIRECTORY_SEPARATOR.'Public';
try {
if (is_link($public_folder)) {
unlink($public_folder);
}
} catch (\Exception $e) {
// Do nothing.
}
\Helper::unzip($module_archive, \Module::getPath());
} catch (\Exception $e) {
// We will show this as floating message on page reload
$response['msg'] = $e->getMessage();
}
// Check if extracted module exists
\Module::clearCache();
$module = \Module::findByAlias($alias);
if (!$module) {
$download_error = true;
}
}
// Remove archive
if (file_exists($module_archive)) {
\File::delete($module_archive);
}
if ($download_error) {
$response['reload'] = true;
if ($response['msg']) {
\Session::flash('flash_error_floating', $response['msg']);
}
\Session::flash('flash_error_unescaped', __('Error occurred downloading the module. Please :%a_being%download:%a_end% module manually and extract into :folder', ['%a_being%' => '<a href="'.$license_details['download_link'].'" target="_blank">', '%a_end%' => '</a>', 'folder' => '<strong>'.\Module::getPath().'</strong>']));
}
} elseif ($license_details['status'] && $response['msg'] = $this->getErrorMessage($license_details['status'])) {
//$response['msg'] = ;
} else {
$response['msg'] = __('Error occurred. Please try again later.');
if ($update_result['download_msg']) {
\Session::flash('flash_error_unescaped', $update_result['download_msg']);
}
}
// Install updated module
if (!$response['msg'] && !$download_error) {
$outputLog = new BufferedOutput();
\Artisan::call('freescout:module-install', ['module_alias' => $alias], $outputLog);
$output = $outputLog->fetch();
// Get module name
$name = '?';
if ($module) {
$name = $module->getName();
}
// Install updated module.
if ($update_result['output'] || $update_result['status']) {
$type = 'danger';
$msg = __('Error occurred activating ":name" module', ['name' => $name]);
if (session('flashes_floating') && is_array(session('flashes_floating'))) {
// If there was any error, module has been deactivated via modules.register_error filter
$msg = '';
foreach (session('flashes_floating') as $flash) {
$msg .= $flash['text'].' ';
}
} elseif (strstr($output, 'Configuration cached successfully')) {
$msg = $update_result['msg'];
if ($update_result['status'] == 'success') {
$type = 'success';
$msg = __('":name" module successfully updated!', ['name' => $name]);
} else {
// Deactivate module
\App\Module::setActive($alias, false);
\Artisan::call('freescout:clear-cache');
$msg = $update_result['msg_success'];
}
// \Session::flash does not work after BufferedOutput
$flash = [
'text' => '<strong>'.$msg.'</strong><pre class="margin-top">'.$output.'</pre>',
'text' => '<strong>'.$msg.'</strong><pre class="margin-top">'.$update_result['output'].'</pre>',
'unescaped' => true,
'type' => $type,
];
@@ -598,6 +648,41 @@ class ModulesController extends Controller
break;
case 'update_all':
$update_all_flashes = [];
foreach ($request->aliases as $alias) {
$update_result = \App\Module::updateModule($alias);
$type = 'danger';
$msg = $update_result['msg'];
if ($update_result['status'] == 'success') {
$type = 'success';
$msg = $update_result['msg_success'];
} elseif ($update_result['download_msg']) {
$msg .= '<br/>'.$update_result['download_msg'];
}
$text = '<strong>'.$update_result['module_name'].':</strong> '.$msg;
if (trim($update_result['output'])) {
$text .= '<pre class="margin-top">'.$update_result['output'].'</pre>';
}
// \Session::flash does not work after BufferedOutput
$update_all_flashes[] = [
'text' => $text,
'unescaped' => true,
'type' => $type,
];
}
if ($update_all_flashes) {
\Cache::forever('modules_flash', $update_all_flashes);
}
$response['status'] = 'success';
break;
default:
$response['msg'] = 'Unknown action';
break;
@@ -609,40 +694,4 @@ class ModulesController extends Controller
return \Response::json($response);
}
public function getErrorMessage($code, $result = null)
{
$msg = '';
switch ($code) {
case 'missing':
$msg = __('License key does not exist');
break;
case 'license_not_activable':
$msg = __("You have to activate each bundle's module separately");
break;
case 'disabled':
$msg = __('License key has been revoked');
break;
case 'no_activations_left':
$msg = __('No activations left for this license key').' ('.__('Use Deactivate License button to transfer license key from another domain').')';
break;
case 'expired':
$msg = __('License key has expired');
break;
case 'key_mismatch':
$msg = __('License key belongs to another module');
break;
case 'invalid_item_id':
$msg = __('Module not found in the modules directory');
break;
default:
if ($result && !empty($result['error'])) {
$msg = __('Error code:'.' '.$result['error']);
}
break;
}
return $msg;
}
}

View File

@@ -27,34 +27,56 @@ class OpenController extends Controller
*
* @return \Illuminate\Http\Response
*/
public function userSetup($hash)
public function userSetup($hash, $invite_sent_at)
{
if (auth()->user()) {
return redirect()->route('dashboard');
}
$user = User::where('invite_hash', $hash)->first();
// https://github.com/freescout-help-desk/freescout/security/advisories/GHSA-jqj5-r72v-v29g
$hash = trim($hash);
$user = null;
if (strlen($hash) == User::INVITE_HASH_LENGTH) {
$user = User::where('invite_hash', $hash)->first();
}
if ($user && $user->locale) {
\Helper::setLocale($user->locale);
}
if ($user && !$this->isInviteExpirationValid($invite_sent_at, $user)) {
$user = null;
}
return view('open/user_setup', ['user' => $user]);
}
/**
* Save user from invitation.
*/
public function userSetupSave($hash, Request $request)
public function userSetupSave($hash, $invite_sent_at, Request $request)
{
if (auth()->user()) {
return redirect()->route('dashboard');
}
$user = User::where('invite_hash', $hash)->first();
// https://github.com/freescout-help-desk/freescout/security/advisories/GHSA-jqj5-r72v-v29g
$hash = trim($hash);
$user = null;
if (strlen($hash) == User::INVITE_HASH_LENGTH) {
$user = User::where('invite_hash', $hash)->first();
}
if (!$user) {
abort(404);
}
if (!$this->isInviteExpirationValid($invite_sent_at, $user)) {
\Helper::denyAccess();
}
$validator = Validator::make($request->all(), [
'email' => 'required|string|email|max:100|unique:users,email,'.$user->id,
'password' => 'required|string|min:8|confirmed',
@@ -82,19 +104,20 @@ class OpenController extends Controller
});
if ($validator->fails()) {
return redirect()->route('user_setup', ['hash' => $hash])
return redirect()->route('user_setup', ['hash' => $hash, 'invite_sent_at' => $invite_sent_at])
->withErrors($validator)
->withInput();
}
$request_data = $request->all();
// Do not allow user to set his role
if (isset($request_data['role'])) {
unset($request_data['role']);
}
if (isset($request_data['photo_url'])) {
unset($request_data['photo_url']);
}
$request_data = [
'email' => $request->email,
'password' => $request->password,
'job_title' => $request->job_title,
'phone' => $request->phone,
'timezone' => $request->timezone,
'time_format' => $request->time_format,
];
$user->fill($request_data);
$user->password = bcrypt($request->password);
@@ -108,26 +131,53 @@ class OpenController extends Controller
// Login user
Auth::guard()->login($user);
\Session::flash('flash_success_floating', __('Welcome to :company_name!', ['company_name' => Option::getCompanyName()]));
\Session::flash('flash_success_floating', __('Welcome to :company_name!', ['company_name' => htmlspecialchars(Option::getCompanyName())]));
return redirect()->route('dashboard');
}
public function isInviteExpirationValid($invite_sent_at_encrypted, $user)
{
$invite_sent_at = \Helper::decrypt($invite_sent_at_encrypted, $user->password);
if ($invite_sent_at == $invite_sent_at_encrypted) {
return false;
}
if (!$invite_sent_at
|| !is_numeric($invite_sent_at)
|| (int)$invite_sent_at < time() - User::INVITE_TTL_DAYS*86400
) {
return false;
}
return true;
}
/*
* Set a thread as read by customer
*/
public function setThreadAsRead($conversation_id, $thread_id)
public function setThreadAsRead($conversation_id, $thread_id, $hash)
{
$mark_as_read = true;
$conversation = Conversation::findOrFail($conversation_id);
$thread = Thread::findOrFail($thread_id);
if ((int)$thread->conversation_id !== (int)$conversation_id) {
$mark_as_read = false;
} elseif (!\Helper::hashEquals($hash, Thread::getOpenTrackingHash($thread, $conversation, $conversation->mailbox))) {
$mark_as_read = false;
}
// We only track the first opening
if (empty($thread->opened_at)) {
if ($mark_as_read && empty($thread->opened_at)) {
$thread->opened_at = date('Y-m-d H:i:s');
$thread->save();
\Eventy::action('thread.opened', $thread, $conversation);
}
// We return always the same result for security purposes.
// Create a 1x1 ttransparent pixel and return it
$pixel = sprintf('%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c', 71, 73, 70, 56, 57, 97, 1, 0, 1, 0, 128, 255, 0, 192, 192, 192, 0, 0, 0, 33, 249, 4, 1, 0, 0, 0, 0, 44, 0, 0, 0, 0, 1, 0, 1, 0, 0, 2, 2, 68, 1, 0, 59);
$response = \Response::make($pixel, 200);
@@ -166,8 +216,15 @@ class OpenController extends Controller
->firstOrFail();
}
// Only allow download if the attachment is public or if the token matches the hash of the contents
if ($token != $attachment->getToken() && (bool)$attachment->public !== true) {
// Check attachment name.
if (mb_strtolower(trim($attachment->file_name)) != mb_strtolower(trim($file_name))) {
return \Helper::denyAccess();
}
// Links to atachments without token have been disabled.
// https://github.com/freescout-help-desk/freescout/security/advisories/GHSA-wg74-ww4w-2qpc
//if ($token != $attachment->getToken() && $attachment->token_type != Attachment::TOKEN_TYPE_LEGACY) {
if (!\Helper::hashEquals($token, $attachment->getToken())) {
return \Helper::denyAccess();
}
@@ -183,36 +240,93 @@ class OpenController extends Controller
$allowed_mime_type = false;
foreach (config('app.viewable_mime_types') as $mime_type) {
if (preg_match('#'.$mime_type.'#', $attachment->mime_type)) {
if (preg_match('#^'.$mime_type.'$#', $attachment->mime_type)) {
$allowed_mime_type = true;
break;
}
}
if ($allowed_mime_type) {
foreach (config('app.non_viewable_mime_types') as $mime_type) {
if (preg_match('#^'.$mime_type.'$#', $attachment->mime_type)) {
$allowed_mime_type = false;
break;
}
}
}
if (!$allowed_mime_type) {
$view_attachment = false;
}
}
// CSP header for extra security.
$csp_header_value = "script-src 'none'; frame-src 'none'; object-src 'none'; font-src 'none'; connect-src 'none'; media-src 'self'; form-action 'none'; base-uri 'none'; sandbox";
// https://github.com/freescout-help-desk/freescout/issues/5281
//
// The bare sandbox directive gives the attachment document an opaque
// ("null") origin. The media element the browser's built-in player creates
// then requests the file cross-origin and is blocked by CORS, so the player
// appears but never plays. allow-same-origin keeps the real origin.
// nosniff replaces the protection sandbox was providing here: the browser
// is not allowed to reinterpret an audio/* response as HTML.
// https://github.com/freescout-help-desk/freescout/pull/5597
if (preg_match('#^audio/.*$#', $attachment->mime_type)) {
//$csp_header_value .= ' allow-scripts';
$csp_header_value .= ' allow-same-origin';
}
// Send file.
if (config('app.download_attachments_via') == 'apache') {
// Send using Apache mod_xsendfile.
$response = response(null)
->header('Content-Type' , $attachment->mime_type)
->header('Content-Type', $attachment->mime_type)
// Laravel adds 'nosniff' by itself.
//->header('X-Content-Type-Options', 'nosniff')
->header('X-Sendfile', $attachment->getLocalFilePath());
if (!$view_attachment) {
$response->header('Content-Disposition', 'attachment; filename="'.$attachment->file_name.'"');
} else {
$response->header('Content-Security-Policy', $csp_header_value);
}
} elseif (config('app.download_attachments_via') == 'nginx') {
// Send using Nginx.
$response = response(null)
->header('Content-Type' , $attachment->mime_type)
->header('Content-Type', $attachment->mime_type)
// Laravel adds 'nosniff' by itself.
//->header('X-Content-Type-Options', 'nosniff')
->header('X-Accel-Redirect', $attachment->getLocalFilePath(false));
if (!$view_attachment) {
$response->header('Content-Disposition', 'attachment; filename="'.$attachment->file_name.'"');
} else {
$response->header('Content-Security-Policy', $csp_header_value);
}
} else {
$response = $attachment->download($view_attachment);
// Send via PHP.
// In this case whether the file is viewed or downloaded is controlled
// via the web server config or /storage/app/public/.htaccess.
$headers = [
'Content-Type' => $attachment->mime_type,
// Laravel adds 'nosniff' by itself.
//'X-Content-Type-Options' => 'nosniff',
];
if ($view_attachment) {
$headers['Content-Security-Policy'] = $csp_header_value;
}
// Storage::download() returns a StreamedResponse, which does not
// implement HTTP Range requests: no Accept-Ranges, no 206 responses.
// Browsers disable seeking without it, so audio and video attachments
// can not be rewound or fast forwarded. BinaryFileResponse handles
// Range requests, so use it whenever the file is shown in the browser.
if ($view_attachment && \Helper::isLocalStorage() && $attachment->fileExists()) {
$response = response()->file(
$attachment->getLocalFilePath(),
$headers
);
} else {
$response = $attachment->download($view_attachment, $headers);
}
}
return $response;

View File

@@ -29,11 +29,8 @@ class SecureController extends Controller
public function dashboard()
{
$user = auth()->user();
if (!$user->isAdmin()) {
$mailboxes = $user->mailboxesCanView();
} else {
$mailboxes = $user->mailboxesCanViewWithSettings();
}
$mailboxes = $user->mailboxesCanViewWithSettings();
// Sort by name.
$mailboxes = \Eventy::filter('dashboard.mailboxes', $mailboxes->sortBy('name'));
@@ -112,12 +109,16 @@ class SecureController extends Controller
'type',
'email',
'status',
'conversation',
'message',
'user',
'customer',
];
$activities = SendLog::orderBy('created_at', 'desc')->paginate($page_size);
$activities_query = SendLog::orderBy('created_at', 'desc');
if ($request->get('thread_id')) {
$activities_query->where('thread_id', $request->get('thread_id'));
}
$activities = $activities_query->paginate($page_size);
foreach ($activities as $record) {
$conversation = '';
@@ -132,13 +133,16 @@ class SecureController extends Controller
$status .= '. Message-ID: '.$record->message_id;
}
}
if ($record->smtp_queue_id) {
$status .= '. SMTP ID: '.$record->smtp_queue_id;
}
$logs[] = [
'date' => $record->created_at,
'type' => $record->getMailTypeName(),
'email' => $record->email,
'status' => $status,
'conversation' => $conversation,
'message' => $conversation,
'user' => $record->user,
'customer' => $record->customer,
];
@@ -193,7 +197,7 @@ class SecureController extends Controller
/**
* Upload files and images.
*/
public function upload(Request $request, $allowed_exts = [])
public function upload(Request $request)
{
// 'jpg','gif','png'
$response = [
@@ -213,14 +217,24 @@ class SecureController extends Controller
if (!$response['msg']) {
$upload = Helper::uploadFile($request->file, $allowed_exts);
$filename = basename($upload);
$uploaded_file_path = '';
try {
$uploaded_file_path = Helper::uploadFile($request->file);
} catch (\Exception $e) {
if ($e->getCode() == \Helper::EXCEPTION_NOT_ALLOWED_FILE_EXTENSION) {
$response['msg'] = $e->getMessage();
}
}
if ($upload) {
$response['status'] = 'success';
$response['url'] = Helper::uploadedFileUrl($filename);
} else {
$response['msg'] = __('Error occurred uploading file');
if (!$response['msg']) {
$filename = basename($uploaded_file_path);
if ($uploaded_file_path) {
$response['status'] = 'success';
$response['url'] = Helper::uploadedFileUrl($filename);
} else {
$response['msg'] = __('Error occurred uploading file');
}
}
}

View File

@@ -94,6 +94,7 @@ class SettingsController extends Controller
],
],
'validator_rules' => [
'settings.mail_host' => 'safehost',
'settings.mail_from' => 'required|email',
],
'settings' => [
@@ -103,7 +104,10 @@ class SettingsController extends Controller
'mail_password' => [
'safe_password' => true,
'encrypt' => true,
]
],
// 'use_mail_date_on_fetching' => [
// 'env' => 'APP_USE_MAIL_DATE_ON_FETCHING',
// ],
],
];
break;
@@ -113,6 +117,9 @@ class SettingsController extends Controller
'custom_number' => [
'env' => 'APP_CUSTOM_NUMBER',
],
'max_message_size' => [
'env' => 'APP_MAX_MESSAGE_SIZE',
],
'email_conv_history' => [
'env' => 'APP_EMAIL_CONV_HISTORY',
],
@@ -129,6 +136,9 @@ class SettingsController extends Controller
'env' => 'APP_USER_PERMISSIONS',
'env_encode' => true,
],
'limit_user_customer_visibility' => [
'env' => 'APP_LIMIT_USER_CUSTOMER_VISIBILITY',
],
],
];
break;
@@ -194,9 +204,11 @@ class SettingsController extends Controller
'next_ticket' => (Option::get('next_ticket') >= Conversation::max('number') + 1) ? Option::get('next_ticket') : Conversation::max('number') + 1,
'custom_number' => (int)config('app.custom_number'),
'user_permissions' => User::getGlobalUserPermissions(),
'limit_user_customer_visibility' => (int)config('app.limit_user_customer_visibility'),
'email_branding' => Option::get('email_branding'),
'open_tracking' => Option::get('open_tracking'),
'email_conv_history' => config('app.email_conv_history'),
'max_message_size' => config('app.max_message_size'),
'email_user_history' => config('app.email_user_history'),
'enrich_customer_data' => Option::get('enrich_customer_data'),
'time_format' => Option::get('time_format', User::TIME_FORMAT_24),
@@ -214,6 +226,7 @@ class SettingsController extends Controller
'mail_password' => \Helper::decrypt(Option::get('mail_password', \Config::get('mail.password'))),
'mail_encryption' => Option::get('mail_encryption', \Config::get('mail.encryption')),
'fetch_schedule' => config('app.fetch_schedule'),
//'use_mail_date_on_fetching' => config('app.use_mail_date_on_fetching'),
];
break;
case 'alerts':
@@ -224,11 +237,13 @@ class SettingsController extends Controller
'alert_logs',
'alert_logs_names',
'alert_logs_period',
'alert_logs_fetch_min_occurrences',
'subscription_defaults',
], [
'alert_logs_names' => [],
'alert_logs' => config('app.alert_logs'),
'alert_logs_period' => config('app.alert_logs_period'),
'alert_logs_names' => [],
'alert_logs' => config('app.alert_logs'),
'alert_logs_period' => config('app.alert_logs_period'),
'alert_logs_fetch_min_occurrences' => \App\Console\Commands\LogsMonitor::FETCH_ERRORS_MIN_OCCURRENCES_DEFAULT,
]);
break;
default:
@@ -305,7 +320,7 @@ class SettingsController extends Controller
$env_value = json_encode($env_value);
}
if (!empty($settings_params[$option_name]['encrypt'])) {
if ($env_value !== '' && !empty($settings_params[$option_name]['encrypt'])) {
$env_value = encrypt($env_value);
}
@@ -386,17 +401,22 @@ class SettingsController extends Controller
}
if (!$response['msg']) {
$test_result = false;
$test_result = [
'status' => 'error',
];
try {
$test_result = \MailHelper::sendTestMail($request->to);
} catch (\Exception $e) {
$response['msg'] = $e->getMessage();
$test_result['msg'] = $e->getMessage();
}
if (!$test_result && !$response['msg']) {
$response['msg'] = __('Error occurred sending email. Please check your mail server logs for more details.');
if ($test_result['status'] == 'error') {
$response['msg'] = $test_result['msg']
?: __('Error occurred sending email. Please check your mail server logs for more details.');
}
$response['log'] = $test_result['log'] ?? '';
}
if (!$response['msg']) {

View File

@@ -53,10 +53,16 @@ class SystemController extends Controller
// Check if cache files are writable.
$non_writable_cache_file = '';
if (function_exists('shell_exec')) {
$non_writable_cache_file = shell_exec('find '.base_path('storage/framework/cache/data/').' -type f | xargs -I {} sh -c \'[ ! -w "{}" ] && echo {}\' 2>&1 | head -n 1');
$non_writable_cache_file = trim($non_writable_cache_file ?? '');
if (!strstr($non_writable_cache_file, base_path('storage/framework/cache/data/'))) {
$non_writable_cache_file = '';
$non_writable_cache_file = \Helper::shellExec('find '.base_path('storage/framework/cache/data/').' -type f | xargs -I {} sh -c \'[ ! -w "{}" ] && echo {}\' 2>&1 | head -n 1');
if ($non_writable_cache_file === false) {
$non_writable_cache_file = 'Could not execute command via shell_exec()';
} else {
$non_writable_cache_file = trim($non_writable_cache_file ?? '');
// Leave only one line (in case head -n 1 does not work)
$non_writable_cache_file = preg_replace("#[\r\n].+#m", '', $non_writable_cache_file);
if (!strstr($non_writable_cache_file, base_path('storage/framework/cache/data/'))) {
$non_writable_cache_file = '';
}
}
}
@@ -78,14 +84,14 @@ class SystemController extends Controller
$env_is_writable = is_writable(base_path('.env'));
// Jobs
$queued_jobs = \App\Job::orderBy('created_at', 'desc')->get();
$failed_jobs = \App\FailedJob::orderBy('failed_at', 'desc')->get();
$queued_jobs = \App\Job::orderBy('created_at', 'desc')->limit(100)->get();
$failed_jobs = \App\FailedJob::orderBy('failed_at', 'desc')->limit(100)->get();
$failed_queues = $failed_jobs->pluck('queue')->unique();
// Commands
$commands_list = [
'freescout:fetch-emails' => 'freescout:fetch-emails',
\Helper::getWorkerIdentifier() => 'queue:work'
\Helper::getWorkerIdentifier() => 'queue:work',
];
foreach ($commands_list as $command_identifier => $command_name) {
$status_texts = [];
@@ -95,19 +101,29 @@ class SystemController extends Controller
$running_commands = 0;
try {
$processes = preg_split("/[\r\n]/", shell_exec("ps aux | grep '{$command_identifier}'"));
$pids = [];
foreach ($processes as $process) {
$process = trim($process);
preg_match("/^[\S]+\s+([\d]+)\s+/", $process, $m);
if (empty($m)) {
// Another format (used in Docker image).
// 1713 nginx 0:00 /usr/bin/php82...
preg_match("/^([\d]+)\s+[\S]+\s+/", $process, $m);
}
if (!preg_match("/(sh \-c|grep )/", $process) && !empty($m[1])) {
$running_commands++;
$pids[] = $m[1];
$ps_output = \Helper::shellExec("ps auxww | grep '{$command_identifier}'");
if ($ps_output === false) {
$commands[] = [
'name' => $command_name,
'status' => 'error',
'status_text' => __h('Could not execute command via shell_exec()'),
];
} else {
$processes = preg_split("/[\r\n]/", $ps_output);
$pids = [];
foreach ($processes as $process) {
$process = trim($process);
preg_match("/^[\S]+\s+([\d]+)\s+/", $process, $m);
if (empty($m)) {
// Another format (used in Docker image).
// 1713 nginx 0:00 /usr/bin/php82...
preg_match("/^([\d]+)\s+[\S]+\s+/", $process, $m);
}
if (!preg_match("/(sh \-c|grep )/", $process) && !empty($m[1])) {
$running_commands++;
$pids[] = $m[1];
}
}
}
} catch (\Exception $e) {
@@ -117,7 +133,7 @@ class SystemController extends Controller
$commands[] = [
'name' => $command_name,
'status' => 'success',
'status_text' => __('Running'),
'status_text' => __h('Running'),
];
continue;
} elseif ($running_commands > 1) {
@@ -127,14 +143,14 @@ class SystemController extends Controller
$commands[] = [
'name' => $command_name,
'status' => 'error',
'status_text' => __(':number commands were running at the same time. Commands have been restarted', ['number' => $running_commands]),
'status_text' => __h(':number commands were running at the same time. Commands have been restarted', ['number' => htmlspecialchars($running_commands)]),
];
} else {
unset($pids[0]);
$commands[] = [
'name' => $command_name,
'status' => 'error',
'status_text' => __(':number commands are running at the same time. Please stop extra commands by executing the following console command:', ['number' => $running_commands]).' kill '.implode(' | kill ', $pids),
'status_text' => __h(':number commands are running at the same time. Please stop extra commands by executing the following console command:', ['number' => htmlspecialchars($running_commands)]).' kill '.implode(' | kill ', $pids),
];
}
continue;
@@ -149,7 +165,7 @@ class SystemController extends Controller
$date = Carbon::createFromTimestamp($last_run);
$date_text = User::dateFormat($date);
}
$status_texts[] = __('Last run:').' '.$date_text;
$status_texts[] = __h('Last run:').' '.htmlspecialchars($date_text);
$date_text = '?';
$last_successful_run = Option::get($option_name.'_last_successful_run');
@@ -157,7 +173,7 @@ class SystemController extends Controller
$date_ = Carbon::createFromTimestamp($last_successful_run);
$date_text = User::dateFormat($date);
}
$status_texts[] = __('Last successful run:').' '.$date_text;
$status_texts[] = __h('Last successful run:').' '.htmlspecialchars($date_text);
$status = 'error';
if ($last_successful_run && $last_run && (int) $last_successful_run >= (int) $last_run) {
@@ -167,7 +183,7 @@ class SystemController extends Controller
// If queue:work is not running, clear cache to let it start if something is wrong with the mutex
if ($command_name == 'queue:work' && !$last_successful_run) {
$status_texts[] = __('Try to :%a_start%clear cache:%a_end% to force command to start.', ['%a_start%' => '<a href="'.route('system.tools').'" target="_blank">', '%a_end%' => '</a>']);
$status_texts[] = __h('Try to :%a_start%clear cache:%a_end% to force command to start.', ['%a_start%' => '<a href="'.route('system.tools').'" target="_blank">', '%a_end%' => '</a>']);
// This sometimes makes Status page open as non logged in user.
//\Artisan::call('freescout:clear-cache', ['--doNotGenerateVars' => true]);
}
@@ -198,6 +214,11 @@ class SystemController extends Controller
$latest_version = \Config::get('app.version');
}
// Detect missing migrations.
$migrations_output = \Helper::runCommand('migrate:status');
preg_match_all("#\| N \| ([^\|]+)\|#", $migrations_output, $migrations_m);
$missing_migrations = $migrations_m[1] ?? [];
return view('system/status', [
'commands' => $commands,
'queued_jobs' => $queued_jobs,
@@ -212,6 +233,8 @@ class SystemController extends Controller
'public_symlink_exists' => $public_symlink_exists,
'env_is_writable' => $env_is_writable,
'non_writable_cache_file' => $non_writable_cache_file,
'missing_migrations' => $missing_migrations,
'invalid_symlinks' => \App\Module::checkSymlinks(),
]);
}
@@ -281,6 +304,7 @@ class SystemController extends Controller
$params = [];
$params['--days'] = (int)$request->days;
$params['--unseen'] = (int)$request->unseen;
$params['--debug'] = (int)$request->debug;
\Artisan::call('freescout:fetch-emails', $params, $outputLog);
break;
@@ -368,18 +392,24 @@ class SystemController extends Controller
*/
public function cron(Request $request)
{
if (empty($request->hash) || $request->hash != \Helper::getWebCronHash()) {
if (empty($request->hash) || !\Helper::hashEquals($request->hash, \Helper::getWebCronHash())) {
abort(404);
}
$outputLog = new BufferedOutput();
\Artisan::call('schedule:run', [], $outputLog);
$output = $outputLog->fetch();
return response($output, 200)->header('Content-Type', 'text/plain');
preg_match_all("#'artisan'\s+([^\s>]+)#", $output ?? '', $m);
$commands = $m[1] ?? [];
$result = count($commands)." commands executed:\r\n".(count($commands) ? '- ' : '').implode("\r\n- ", $commands);
return response($result, 200)->header('Content-Type', 'text/plain');
}
/**
* Ajax HTML.
* Content Security Policy header is sent via ContentSecurityPolicy middleware.
*/
public function ajaxHtml(Request $request)
{
@@ -394,10 +424,10 @@ class SystemController extends Controller
$payload = json_decode($job->payload, true);
if (!empty($payload['data']['command'])) {
$html .= '<pre>'.print_r(unserialize($payload['data']['command']), 1).'</pre>';
$html .= '<pre>'.\Helper::stripDangerousTags(print_r(unserialize($payload['data']['command'], ['allowed_classes' => false]), 1)).'</pre>';
}
$html .= '<pre>'.$job->exception.'</pre>';
$html .= '<pre>'.\Helper::stripDangerousTags($job->exception).'</pre>';
return response($html);
}

View File

@@ -69,9 +69,13 @@ class TranslateController extends BaseController
public function postDownload()
{
$this->manager->exportTranslations('*', false);
$file_name = 'lang.zip';
// Archive langs folder
$archive_path = \Helper::createZipArchive(base_path().DIRECTORY_SEPARATOR.'resources/lang', $file_name, 'lang');
$archive_path = \Helper::createZipArchive(base_path().DIRECTORY_SEPARATOR.'resources/lang', 'lang.zip', 'lang');
$hash = substr(\Helper::hmacHash($archive_path), 0, 10);
$file_name = 'lang_'.$hash.'.zip';
$public_path = storage_path('app/public/'.$file_name);
\File::copy($archive_path, $public_path);

View File

@@ -2,7 +2,6 @@
namespace App\Http\Controllers;
use App\Events\UserDeleted;
use App\Folder;
use App\Mailbox;
use App\Subscription;
@@ -33,7 +32,14 @@ class UsersController extends Controller
{
$this->authorize('create', 'App\User');
$users = User::nonDeleted()->get();
$users_query = User::nonDeleted();
// Exclude admins from list for non-admins.
if (!auth()->user()->isAdmin()) {
$users_query->where('role', User::ROLE_USER);
}
$users = $users_query->get();
$users = User::sortUsers($users);
return view('users/users', ['users' => $users]);
@@ -45,7 +51,7 @@ class UsersController extends Controller
public function create()
{
$this->authorize('create', 'App\User');
$mailboxes = Mailbox::all();
$mailboxes = auth()->user()->mailboxesCanView();
return view('users/create', ['mailboxes' => $mailboxes]);
}
@@ -63,7 +69,7 @@ class UsersController extends Controller
$rules = [
'first_name' => 'required|string|max:20',
'last_name' => 'required|string|max:30',
'last_name' => 'max:30',
'email' => 'required|string|email|max:100|unique:users',
//'role' => ['required', Rule::in(array_keys(User::$roles))],
];
@@ -86,24 +92,44 @@ class UsersController extends Controller
->withInput();
}
$user = new User();
$user->fill($request->all());
$password = '';
if (empty($request->send_invite)) {
// Set password from request.
$password = $request->password;
} else {
// Set some random password before sending invite.
$password = User::generateRandomPassword();
}
$data = [
'first_name' => $request->first_name,
'last_name' => $request->last_name ?? '',
'email' => $request->email,
'password' => $password,
];
$user = User::create($data);
if (!$auth_user->can('changeRole', $user)) {
$user->role = User::ROLE_USER;
} elseif (array_key_exists($request->role, User::$roles)) {
$user->role = $request->role;
}
if (empty($request->send_invite)) {
// Set password from request
$user->password = Hash::make($request->password);
} else {
// Set some random password before sending invite
$user->password = Hash::make($user->generateRandomPassword());
}
// Set system timezone.
$user->timezone = config('app.timezone') ?: User::DEFAULT_TIMEZONE;
$user = \Eventy::filter('user.create_save', $user, $request);
$user->save();
$user->mailboxes()->sync($request->mailboxes);
$mailbox_ids = $request->mailboxes ?: [];
if (!$auth_user->isAdmin()) {
// Exlude mailboxes to which current user has no access.
// https://github.com/freescout-help-desk/freescout/security/advisories/GHSA-w36p-9vhj-x273
$mailbox_ids = array_intersect($mailbox_ids, $auth_user->mailboxesIdsCanView());
}
$user->mailboxes()->sync($mailbox_ids);
$user->syncPersonalFolders($request->mailboxes);
// Send invite
@@ -141,7 +167,7 @@ class UsersController extends Controller
public function getUsersForSidebar($except_id)
{
if (auth()->user()->isAdmin()) {
return User::sortUsers(User::nonDeleted()->get());/*->except($except_id)*/;
return User::sortUsers(User::nonDeleted()->get()); // ->except($except_id);
} else {
return [];
}
@@ -161,10 +187,12 @@ class UsersController extends Controller
$user = User::findOrFail($id);
$this->authorize('update', $user);
$auth_user = auth()->user();
// This is also present in PublicController::userSetup
$validator = Validator::make($request->all(), [
'first_name' => 'required|string|max:20',
'last_name' => 'required|string|max:30',
'last_name' => 'max:30',
'email' => 'required|string|email|max:100|unique:users,email,'.$id,
//'emails' => 'max:100',
'job_title' => 'max:100',
@@ -213,7 +241,7 @@ class UsersController extends Controller
}
// Save language into session.
if (auth()->user()->id == $id && $request->locale) {
if ($auth_user->id == $id && $request->locale) {
session()->put('user_locale', $request->locale);
}
@@ -222,7 +250,7 @@ class UsersController extends Controller
if (isset($request_data['photo_url'])) {
unset($request_data['photo_url']);
}
if (!auth()->user()->can('changeRole', $user)) {
if (!$auth_user->can('changeRole', $user)) {
unset($request_data['role']);
}
if ($user->status != User::STATUS_DELETED) {
@@ -232,10 +260,51 @@ class UsersController extends Controller
$request_data['status'] = User::STATUS_ACTIVE;
}
}
// Sanitize $request_data array.
// $allowed_fields = [
// 'first_name',
// 'last_name',
// 'email',
// 'emails',
// 'job_title',
// 'phone',
// 'locale',
// 'timezone'
// 'time_format',
// 'photo_url',
// ];
$admin_fields = [
'role',
'status',
'email',
'only_assigned_tickets',
];
$nonfillable_fields = [
'type',
'password',
];
if (!$auth_user->isAdmin()) {
foreach ($admin_fields as $field) {
if (isset($request_data[$field])) {
unset($request_data[$field]);
}
}
}
foreach ($nonfillable_fields as $field) {
if (isset($request_data[$field])) {
unset($request_data[$field]);
}
}
$user->setData($request_data);
if (empty($request->input('enable_kb_shortcuts'))) {
$user->enable_kb_shortcuts = false;
if ($auth_user->isAdmin() && !$user->isAdmin()) {
if (!empty($request_data['only_assigned_tickets'])) {
$user->addPermission(User::PERM_ONLY_ASSIGNED_TICKETS, false);
} else {
$user->removePermission(User::PERM_ONLY_ASSIGNED_TICKETS, false);
}
}
$user = \Eventy::filter('user.save_profile', $user, $request);
@@ -259,7 +328,11 @@ class UsersController extends Controller
$user = User::findOrFail($id);
$mailboxes = Mailbox::all();
if ($user->isDeleted()) {
abort(404);
}
$mailboxes = Mailbox::all()->sortBy('name');
$users = $this->getUsersForSidebar($id);
@@ -286,7 +359,7 @@ class UsersController extends Controller
$user = User::findOrFail($id);
$user->mailboxes()->sync($request->mailboxes);
$user->mailboxes()->sync($request->mailboxes ?: []);
$user->syncPersonalFolders($request->mailboxes);
// Save permissions.
@@ -315,8 +388,12 @@ class UsersController extends Controller
public function notifications($id)
{
$user = User::findOrFail($id);
$this->authorize('update', $user);
$this->authorize('updateNotifications', $user);
if ($user->isDeleted()) {
abort(404);
}
$subscriptions = $user->subscriptions()->select('medium', 'event')->get();
$person = '';
@@ -344,7 +421,7 @@ class UsersController extends Controller
public function notificationsSave($id, Request $request)
{
$user = User::findOrFail($id);
$this->authorize('update', $user);
$this->authorize('updateNotifications', $user);
Subscription::saveFromArray($request->subscriptions, $user->id);
@@ -479,108 +556,26 @@ class UsersController extends Controller
$response['msg'] = __('User not found');
} elseif (!$auth_user->can('delete', $user)) {
$response['msg'] = __('Not enough permissions');
} elseif ($auth_user->id == $user->id) {
// Do not allow admin delete himself.
$response['msg'] = __('Not enough permissions');
}
// Check if the user is the only one admin
if (!$response['msg'] && $user->isAdmin()) {
$admins_count = User::where('role', User::ROLE_ADMIN)->count();
if ($admins_count < 2) {
$response['msg'] = __('Administrator can not be deleted');
}
}
// - not needed as only admin can delete users and
// current admin can not delete himself.
// if (!$response['msg'] && $user->isAdmin()) {
// $admins_count = User::where('role', User::ROLE_ADMIN)->count();
// if ($admins_count < 2) {
// $response['msg'] = __('Administrator can not be deleted');
// }
// }
if (!$response['msg']) {
// We have to process conversations one by one to move them to Unassigned folder,
// as conversations may be in different mailboxes
// $user->conversations()->update(['user_id' => null, 'folder_id' => ]);
$mailbox_unassigned_folders = [];
$user->deleteUser($auth_user, $request->assign_user);
$user->conversations->each(function ($conversation) use ($auth_user, $request) {
// We don't fire ConversationUserChanged event to avoid sending notifications to users
if (!empty($request->assign_user)
&& !empty($request->assign_user[$conversation->mailbox_id])
&& (int) $request->assign_user[$conversation->mailbox_id] != -1
) {
// Set assignee.
// In this case conversation stays assigned, just assignee changes.
$conversation->user_id = $request->assign_user[$conversation->mailbox_id];
} else {
// Make convesation Unassigned.
// Unset assignee.
// Maybe use changeUser() here.
$conversation->user_id = null;
if ($conversation->isPublished()
&& ($conversation->isActive() || $conversation->isPending())
) {
// Change conversation folder to UNASSIGNED.
$folder_id = null;
if (!empty($mailbox_unassigned_folders[$conversation->mailbox_id])) {
$folder_id = $mailbox_unassigned_folders[$conversation->mailbox_id];
} else {
$folder = $conversation->mailbox->folders()
->where('type', Folder::TYPE_UNASSIGNED)
->first();
if ($folder) {
$folder_id = $folder->id;
$mailbox_unassigned_folders[$conversation->mailbox_id] = $folder_id;
}
}
if ($folder_id) {
$conversation->folder_id = $folder_id;
}
}
}
$conversation->save();
// Create lineitem thread
$thread = new Thread();
$thread->conversation_id = $conversation->id;
$thread->user_id = $conversation->user_id;
$thread->type = Thread::TYPE_LINEITEM;
$thread->state = Thread::STATE_PUBLISHED;
$thread->status = Thread::STATUS_NOCHANGE;
$thread->action_type = Thread::ACTION_TYPE_USER_CHANGED;
$thread->source_via = Thread::PERSON_USER;
$thread->source_type = Thread::SOURCE_TYPE_WEB;
$thread->customer_id = $conversation->customer_id;
$thread->created_by_user_id = $auth_user->id;
$thread->save();
});
// Recalculate counters for folders
//if ($user->isAdmin()) {
// Admin has access to all mailboxes
Mailbox::all()->each(function ($mailbox) {
$mailbox->updateFoldersCounters();
});
// } else {
// $user->mailboxes->each(function ($mailbox) {
// $mailbox->updateFoldersCounters();
// });
// }
// Disconnect user from mailboxes.
$user->mailboxes()->sync([]);
$user->folders()->delete();
$user->status = \App\User::STATUS_DELETED;
// Update email.
$email_suffix = User::EMAIL_DELETED_SUFFIX.date('YmdHis');
// We have to truncate email to avoid "Data too long" error.
$user->email = mb_substr($user->email, 0, User::EMAIL_MAX_LENGTH - mb_strlen($email_suffix)).$email_suffix;
$user->save();
event(new UserDeleted($user, $auth_user));
\Session::flash('flash_success_floating', __('User deleted').': '.$user->getFullName());
\Session::flash('flash_success_floating', __('User deleted').': '.htmlspecialchars($user->getFullName()));
$response['status'] = 'success';
}
@@ -588,6 +583,7 @@ class UsersController extends Controller
default:
$response['msg'] = 'Unknown action';
$response = \Eventy::filter('users.ajax.response_default', $response, $request);
break;
}

View File

@@ -18,6 +18,7 @@ class Kernel extends HttpKernel
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
\App\Http\Middleware\TrimStrings::class,
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
\App\Http\Middleware\TrustHosts::class,
\App\Http\Middleware\TrustProxies::class,
\App\Http\Middleware\ResponseHeaders::class,
\App\Http\Middleware\TerminateHandler::class,
@@ -39,8 +40,18 @@ class Kernel extends HttpKernel
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
\App\Http\Middleware\HttpsRedirect::class,
\App\Http\Middleware\CheckBrowser::class,
\App\Http\Middleware\Localize::class,
\App\Http\Middleware\LogoutIfDeleted::class,
\App\Http\Middleware\FrameGuard::class,
\App\Http\Middleware\CustomHandle::class,
\App\Http\Middleware\ContentSecurityPolicy::class,
],
'open' => [
\Illuminate\Routing\Middleware\SubstituteBindings::class,
\App\Http\Middleware\HttpsRedirect::class,
\App\Http\Middleware\FrameGuard::class,
\App\Http\Middleware\CustomHandle::class,
],

View File

@@ -0,0 +1,33 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Redirect;
class CheckBrowser
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @param string|null $guard
*
* @return mixed
*/
public function handle($request, Closure $next, $guard = null)
{
$result = \Helper::checkBrowser($request);
if ($result['msg']) {
if (config('app.disable_browser_check')) {
//\Log::error($result['msg']);
} else {
abort(403, __($result['msg'].'[display]'));
}
}
return $next($request);
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Redirect;
/**
* On regular pages CSP is added via <meta> tag.
* For /ajax-html/ requests CSP is added as a response header.
*/
class ContentSecurityPolicy
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @param string|null $guard
*
* @return mixed
*/
public function handle($request, Closure $next, $guard = null)
{
$response = $next($request);
if (str_contains($request->path(), '/ajax-html/')) {
$response->header('Content-Security-Policy', \Helper::getCspValue());
}
return $response;
}
}

View File

@@ -16,6 +16,11 @@ class CustomHandle
*/
public function handle($request, Closure $next)
{
// Enable/disable chat mode
if ($request->exists('chat_mode')) {
\Helper::setChatMode((int)$request->chat_mode);
}
// Hook.
\Eventy::action('middleware.web.custom_handle', $request);

View File

@@ -12,6 +12,7 @@ class EncryptCookies extends Middleware
* @var array
*/
protected $except = [
'in_app'
'in_app',
'customers_view',
];
}

View File

@@ -0,0 +1,32 @@
<?php
namespace App\Http\Middleware;
use Closure;
class FrameGuard
{
/**
* Handle the given request and get the response.
*/
public function handle($request, Closure $next)
{
$response = $next($request);
$x_frame_options = config('app.x_frame_options');
if (false !== $x_frame_options
&& $x_frame_options !== 'false'
&& $x_frame_options !== '0'
) {
$value = 'SAMEORIGIN';
if (is_string($x_frame_options) && preg_match("#(DENY|SAMEORIGIN|ALLOW-FROM)#i", $x_frame_options)) {
$value = $x_frame_options;
}
$response->headers->set('X-Frame-Options', $value, false);
}
return $response;
}
}

View File

@@ -11,41 +11,38 @@ use Closure;
use Illuminate\Support\Facades\App;
use Illuminate\Http\Request;
class HttpsRedirect {
class HttpsRedirect
{
/**
* The current proxy header mappings.
*
* @var array
*/
protected $headers = [
Request::HEADER_FORWARDED => 'FORWARDED',
Request::HEADER_X_FORWARDED_FOR => 'X_FORWARDED_FOR',
Request::HEADER_X_FORWARDED_HOST => 'X_FORWARDED_HOST',
Request::HEADER_X_FORWARDED_PORT => 'X_FORWARDED_PORT',
Request::HEADER_X_FORWARDED_PROTO => 'X_FORWARDED_PROTO',
];
// protected $headers = [
// Request::HEADER_FORWARDED => 'FORWARDED',
// Request::HEADER_X_FORWARDED_FOR => 'X_FORWARDED_FOR',
// Request::HEADER_X_FORWARDED_HOST => 'X_FORWARDED_HOST',
// Request::HEADER_X_FORWARDED_PORT => 'X_FORWARDED_PORT',
// Request::HEADER_X_FORWARDED_PROTO => 'X_FORWARDED_PROTO',
// ];
public function handle($request, Closure $next)
{
if (\Helper::isHttps()) {
$request->setTrustedProxies( [ $request->getClientIp() ], array_keys($this->headers));
if (!$request->secure() && strtolower($_SERVER['HTTPS'] ?? '') != 'on'
&& ($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '') != 'https'
&& ($_SERVER['HTTP_CF_VISITOR'] ?? '') != '{"scheme":"https"}'
) {
if (\Helper::isHttps() && !\Helper::isConsole()) {
//$request->setTrustedProxies( [ $request->getClientIp() ], array_keys($this->headers));
//!$request->secure()
if (!\Helper::isCurrentUrlHttps()) {
return redirect()->secure($request->getRequestUri());
}
}
// Correct protocol in $_SERVER
if (\Helper::isHttps()
&& !$request->secure()
//&& !$request->secure()
&& strtolower($_SERVER['HTTPS'] ?? '') != 'on'
) {
$_SERVER['HTTPS'] = 'on';
}
return $next($request);
}
}
}

View File

@@ -6,11 +6,30 @@ use Closure;
class ResponseHeaders
{
private $unwanted_headers = [
'X-Powered-By',
'Server',
];
public function handle($request, Closure $next)
{
$this->removeUnwantedHeaders($this->unwanted_headers);
$response = $next($request);
// Disable caching
// Secure headers.
//$response->headers->set('Referrer-Policy', 'no-referrer-when-downgrade');
$response->headers->set('X-Content-Type-Options', 'nosniff');
// $response->headers->set('X-XSS-Protection', '1; mode=block');
// $response->headers->set('X-Frame-Options', 'DENY');
//$response->headers->set('Content-Security-Policy', "style-src 'self'");
// Instruct browsers to always use HTTPS for all future requests.
if (\Helper::isHttps()) {
$response->headers->set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload');
}
// Disable caching.
if (method_exists($response, 'header')) {
$response->header('Pragma', 'no-cache');
$response->header('Cache-Control', 'no-cache, max-age=0, must-revalidate, no-store');
@@ -18,4 +37,11 @@ class ResponseHeaders
return $response;
}
private function removeUnwantedHeaders($headerList)
{
foreach ($headerList as $header) {
header_remove($header);
}
}
}

View File

@@ -17,14 +17,31 @@ class TokenAuth
*/
public function handle($request, Closure $next)
{
if (!$request->user() && !empty($request->auth_token) && $request->cookie('in_app')) {
try {
$user = User::where(\DB::raw('md5(CONCAT(id, "'.config('app.key').'"))') , $request->auth_token)
->first();
} catch (\Exception $e) {
\Helper::logException($e, '[TokenAuth]');
// This is needed to restore authentication when app session expires.
if (!$request->user() && !empty($request->auth_token) && is_string($request->auth_token) && \Helper::isInApp($request)) {
// Decode token (format: urlencode(base64_encode(user_id:expiry:hash)))
$parts = explode(':', urldecode(base64_decode($request->auth_token)));
if (count($parts) !== 3) {
return $next($request);
}
if (!empty($user)) {
list($user_id, $expiry, $token_hash) = $parts;
// Check expiration.
if (time() > (int)$expiry) {
return $next($request);
}
// Get user.
$user = User::find($user_id);
if (!$user) {
return $next($request);
}
// Verify hash.
$hash = hash_hmac('sha256', $user_id.':'.$expiry, config('app.key').$user->password);
if (hash_equals($hash, $token_hash)) {
\Auth::login($user);
}
}

View File

@@ -0,0 +1,98 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Exception\SuspiciousOperationException;
/**
* https://github.com/freescout-help-desk/freescout/security/advisories/GHSA-822g-7rw5-53xj
*/
class TrustHosts
{
/**
* The application instance.
*
* @var \Illuminate\Contracts\Foundation\Application
*/
protected $app;
/**
* The trusted hosts that have been configured to always be trusted.
*/
protected static $trusted_hosts;
/**
* Create a new middleware instance.
*
* @param \Illuminate\Contracts\Foundation\Application $app
*/
public function __construct(Application $app)
{
$this->app = $app;
}
/**
* Handle the incoming request.
*/
public function handle(Request $request, $next)
{
if (//! $this->app->environment('local') &&
$this->app->runningUnitTests()
) {
return $next($request);
}
// Check if current host matches APP_URL.
list($current_host) = explode(':', request()->getHttpHost());
$current_host = mb_strtolower($current_host);
$app_host = mb_strtolower(\Helper::getDomain());
// Do not check during installation process.
if ($app_host == 'example.com' && preg_match("#^install/#", $request->path())) {
return $next($request);
}
if ($current_host == $app_host) {
return $next($request);
}
// Check hosts from APP_TRUSTED_HOSTS.
$trusted_hosts = explode(',', config('app.trusted_hosts'));
foreach ($trusted_hosts as $host) {
$host = mb_strtolower(trim($host));
if ($host && $current_host == $host) {
return $next($request);
}
}
$is_trusted_host = \Eventy::filter('app.is_trusted_host', false, $current_host);
if ($is_trusted_host) {
return $next($request);
}
// Throws: Untrusted Host...
//Request::setTrustedHosts([$current_host]);
//throw new SuspiciousOperationException(sprintf('Untrusted Host "%s".', $host));
\Helper::denyAccess("Untrusted Host: {$current_host}. Add the following to your .env file and clear cache: APP_TRUSTED_HOSTS ='$current_host'", true);
//return $next($request);
}
/**
* Get a regular expression matching the application URL and all of its subdomains.
*
* @return string|null
*/
// protected function allSubdomainsOfAppUrl()
// {
// if ($host = parse_url($this->app['config']->get('app.url'), PHP_URL_HOST)) {
// return '^(.+\.)?'.preg_quote($host).'$';
// }
// }
}

View File

@@ -2,14 +2,72 @@
namespace App;
use App\Thread;
use Illuminate\Database\Eloquent\Model;
class Job extends Model
{
const UPDATED_AT = null;
public $payload_decoded = null;
/**
* Automatically converted into Carbon dates.
*/
protected $dates = ['created_at', 'available_at', 'reserved_at'];
public function getPayloadDecoded()
{
if ($this->payload_decoded !== null) {
return $this->payload_decoded;
}
$this->payload_decoded = json_decode($this->payload, true);
return $this->payload_decoded;
}
public function getCommand($allowed_classes = [])
{
return self::getPayloadCommand($this->getPayloadDecoded(), $allowed_classes);
}
public function getCommandLastThread()
{
$command = $this->getCommand();
if ($command && !empty($command->threads)) {
return Thread::getLastThread($command->threads);
}
return null;
}
public static function getPayloadCommand($payload, $allowed_classes = [])
{
if (empty($payload['data']) || empty($payload['data']['command'])) {
return null;
}
if (!$allowed_classes) {
$allowed_classes = [
'App\Jobs\SendReplyToCustomer',
'App\Jobs\SendNotificationToUsers',
'App\Jobs\SendAutoReply',
'App\Jobs\SendAlert',
'App\Jobs\SendEmailReplyError',
'Illuminate\Contracts\Database\ModelIdentifier',
];
}
try {
// If some record has been deleted from DB, there will be an error:
// No query results for model [App\Conversation].
return unserialize($payload['data']['command'], ['allowed_classes' => $allowed_classes]);
} catch (\Exception $e) {
return null;
}
}
public static function getTriggerActionName($payload)
{
return preg_replace('/^.*?action";s:\d+:"([^"]+)".*$/s', '$1', $payload['data']['command'] ?? '');
}
}

View File

@@ -77,9 +77,23 @@ class SendAutoReply implements ShouldQueue
$failures = [];
$exception = null;
// Reply from the alias the customer emailed (if "Allow to reply from aliases" is enabled).
$from_alias = '';
$aliases = $this->mailbox->getAliases(true, true);
if (count($aliases)) {
$original_recipients = array_merge($this->thread->getToArray(), $this->thread->getCcArray());
foreach ($original_recipients as $original_recipient) {
$original_recipient = \App\Email::sanitizeEmail($original_recipient);
if ($original_recipient && array_key_exists($original_recipient, $aliases)) {
$from_alias = $original_recipient;
break;
}
}
}
try {
Mail::to([['name' => $this->customer->getFullName(), 'email' => $customer_email]])
->send(new AutoReply($this->conversation, $this->mailbox, $this->customer, $headers));
->send(new AutoReply($this->conversation, $this->mailbox, $this->customer, $headers, $from_alias));
} catch (\Exception $e) {
// We come here in case SMTP server unavailable for example
activity()

View File

@@ -24,6 +24,8 @@ class SendEmailReplyError implements ShouldQueue
public $mailbox;
public $text;
/**
* The number of seconds the job can run before timing out.
* fwrite() function in /vendor/swiftmailer/swiftmailer/lib/classes/Swift/Transport/StreamBuffer.php
@@ -37,11 +39,12 @@ class SendEmailReplyError implements ShouldQueue
*
* @return void
*/
public function __construct($from, $user, $mailbox)
public function __construct($from, $user, $mailbox, $text = null)
{
$this->from = $from;
$this->user = $user;
$this->mailbox = $mailbox;
$this->text = $text;
}
/**
@@ -58,7 +61,7 @@ class SendEmailReplyError implements ShouldQueue
try {
Mail::to([['name' => '', 'email' => $this->from]])
->send(new UserEmailReplyError());
->send(new UserEmailReplyError($this->text));
} catch (\Exception $e) {
// We come here in case SMTP server unavailable for example
activity()

View File

@@ -58,11 +58,7 @@ class SendNotificationToUsers implements ShouldQueue
\App\Misc\Mail::setMailDriver($mailbox, null, $this->conversation);
// Threads has to be sorted here, if sorted before, they come here in wrong order
$this->threads = $this->threads->sortByDesc(function ($item, $key) {
// Threads has to be sorted by created_at and not by id.
// https://github.com/freescout-helpdesk/freescout/issues/2938
return $item->created_at->getTimestamp();
});
$this->threads = Thread::sortThreads($this->threads);
$headers = [];
$last_thread = $this->threads->first();
@@ -76,6 +72,13 @@ class SendNotificationToUsers implements ShouldQueue
return;
}
// Do not send email notifications to support agents
// if the email is a bounce and mail server send limit reached.
// https://github.com/freescout-help-desk/freescout/issues/4806
if ($last_thread->isBounce() && strstr($last_thread->body, 'message limit exceeded')) {
return;
}
// Limit conversation history
if (config('app.email_user_history') == 'last') {
$this->threads = $this->threads->slice(0, 2);
@@ -93,10 +96,13 @@ class SendNotificationToUsers implements ShouldQueue
$headers['X-Auto-Response-Suppress'] = 'All';
// We throw an exception if any of the send attempts throws an exception (connection error, etc)
$global_exception = null;
$last_send_exception = null;
foreach ($this->users as $user) {
// https://github.com/freescout-help-desk/freescout/security/advisories/GHSA-2j2c-g32w-96vm
$user->refresh();
// User can ne deleted from DB.
if (!isset($user->id)) {
continue;
@@ -106,6 +112,10 @@ class SendNotificationToUsers implements ShouldQueue
continue;
}
if (!$user->can('view', $this->conversation)) {
continue;
}
// If for one user sending fails the job is marked as failed and retried after some time.
// So we have to check if notification email has already been successfully sent to this user.
if ($this->attempts() > 1) {
@@ -120,7 +130,7 @@ class SendNotificationToUsers implements ShouldQueue
}
}
$message_id = \App\Misc\Mail::MESSAGE_ID_PREFIX_NOTIFICATION.'-'.$last_thread->id.'-'.$user->id.'-'.time().'@'.$mailbox->getEmailDomain();
$message_id = \App\Misc\Mail::MESSAGE_ID_PREFIX_NOTIFICATION.'-'.$last_thread->id.'-'.$user->id.'-'.\MailHelper::getMessageIdHash($last_thread->id).'@'.$mailbox->getEmailDomain();
$headers['Message-ID'] = $message_id;
// If this is notification on message from customer, set customer as sender name
@@ -151,17 +161,18 @@ class SendNotificationToUsers implements ShouldQueue
Mail::to([['name' => $user->getFullName(), 'email' => $user->email]])
->send(new UserNotification($user, $this->conversation, $this->threads, $headers, $from, $mailbox));
} catch (\Exception $e) {
// We come here in case SMTP server unavailable for example
// We come here if SMTP server unavailable for example
activity()
->causedBy($user)
->withProperties([
'error' => $e->getMessage().'; File: '.$e->getFile().' ('.$e->getLine().')',
'conversation' => '#'.$this->conversation->number.'-'.($last_thread ? $last_thread->id : ''),
])
->useLog(\App\ActivityLog::NAME_EMAILS_SENDING)
->log(\App\ActivityLog::DESCRIPTION_EMAILS_SENDING_ERROR_TO_USER);
$exception = $e;
$global_exception = $e;
$last_send_exception = $e;
}
$status_message = '';
@@ -182,21 +193,33 @@ class SendNotificationToUsers implements ShouldQueue
SendLog::log($last_thread->id, $message_id, $user->email, SendLog::MAIL_TYPE_USER_NOTIFICATION, $status, null, $user->id, $status_message);
}
if ($global_exception) {
if ($last_send_exception) {
// Retry job with delay.
// https://stackoverflow.com/questions/35258175/how-can-i-create-delays-between-failed-queued-job-attempts-in-laravel
if ($this->attempts() < $this->tries) {
// We do not try to resend Bounce messages: https://github.com/freescout-helpdesk/freescout/issues/3156
if ($this->attempts() < $this->tries && !$last_thread->isBounce()) {
if ($this->attempts() == 1) {
// Second attempt after 5 min.
$this->release(300);
} else {
// Mail data has been passed to the mail server but after that
// we have not receved a response from the mail server (due to timeout).
// So we may consider that the email has been successfully sent.
// Otherwise this email may be sent again and again.
if ($this->attempts() >= 3
&& preg_match("#^Connection to .* Timed Out$#", $last_send_exception->getMessage())
&& \MailHelper::$smtp_data_sent
) {
$this->fail($last_send_exception);
}
// Others - after 1 hour.
$this->release(3600);
}
throw $global_exception;
throw $last_send_exception;
} else {
$this->fail($global_exception);
$this->fail($last_send_exception);
return;
}

View File

@@ -3,15 +3,17 @@
namespace App\Jobs;
use App\Mail\ReplyToCustomer;
use App\Customer;
use App\SendLog;
use App\Thread;
use App\Misc\SwiftGetSmtpQueueId;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Mail;
use Webklex\IMAP\Client;
//use Webklex\IMAP\Client;
class SendReplyToCustomer implements ShouldQueue
{
@@ -21,8 +23,12 @@ class SendReplyToCustomer implements ShouldQueue
public $threads;
// Recipient.
public $customer;
// Needed to show proper signature when conversation is being moved between mailboxes.
public $mailbox_change_history = [];
private $failures = [];
private $recipients = [];
private $last_thread = null;
@@ -49,6 +55,7 @@ class SendReplyToCustomer implements ShouldQueue
{
$this->conversation = $conversation;
$this->threads = $threads;
// Recipient.
$this->customer = $customer;
}
@@ -80,10 +87,8 @@ class SendReplyToCustomer implements ShouldQueue
if ($forward_child_thread->isForwarded() && $forward_child_thread->getForwardParentConversation()) {
// Add replies from original conversation.
$forwarded_replies = $forward_child_thread->getForwardParentConversation()->getReplies();
$forwarded_replies = $forwarded_replies->sortByDesc(function ($item, $key) {
return $item->created_at;
});
$forwarded_replies = $forward_child_thread->getForwardParentConversation()->getThreads(null, null, [Thread::TYPE_CUSTOMER, Thread::TYPE_MESSAGE, Thread::TYPE_LINEITEM]);
$forwarded_replies = Thread::sortThreads($forwarded_replies);
$forward_parent_thread = Thread::find($forward_child_thread->getMetaFw(Thread::META_FORWARD_PARENT_THREAD_ID));
if ($forward_parent_thread) {
@@ -95,20 +100,34 @@ class SendReplyToCustomer implements ShouldQueue
}
$this->threads = $this->threads->merge($forwarded_replies);
$is_forward = true;
// In order to show proper signature.
// https://github.com/freescout-help-desk/freescout/issues/5419
$new_mailbox_id = null;
foreach ($this->threads as $reply) {
if ($reply->action_type == Thread::ACTION_TYPE_MOVED_FROM_MAILBOX && is_numeric($reply->action_data)) {
$new_mailbox_id = (int)$reply->action_data;
}
if ($reply->isReply() && $new_mailbox_id !== null) {
$this->mailbox_change_history[$reply->id] = $new_mailbox_id;
$new_mailbox_id = null;
}
}
// Now remove line items.
$this->threads = $this->threads->whereIn('type', [Thread::TYPE_CUSTOMER, Thread::TYPE_MESSAGE]);
}
}
}
// Threads has to be sorted here, if sorted before, they come here in wrong order
$this->threads = $this->threads->sortByDesc(function ($item, $key) {
// Threads has to be sorted by created_at and not by id.
// https://github.com/freescout-helpdesk/freescout/issues/2938
return $item->created_at->getTimestamp();
});
$this->threads = Thread::sortThreads($this->threads);
$new = false;
$headers = [];
$this->last_thread = $this->threads->first();
if ($this->last_thread === null) {
return;
}
@@ -119,6 +138,17 @@ class SendReplyToCustomer implements ShouldQueue
return;
}
// After sending an email we are saving it into "IMAP Folder To Save Outgoing Replies".
// This process may stuck or make SendReplyToCustomer job die with
// "Allowed memory size of NNN bytes exhausted" error.
// https://github.com/freescout-helpdesk/freescout/issues/3632
if ($this->attempts() >= 1 &&
($this->last_thread->send_status == SendLog::STATUS_ACCEPTED
|| $this->last_thread->isSendStatusSuccess())
) {
return;
}
if (count($this->threads) == 1) {
$new = true;
}
@@ -133,6 +163,82 @@ class SendReplyToCustomer implements ShouldQueue
}
}
// In-Reply-To and References headers.
$references = '';
if (!$new && !empty($last_customer_thread) && $last_customer_thread->message_id) {
// Always put real Message-ID into In-Reply-To field.
// https://github.com/freescout-help-desk/freescout/issues/5620
$headers['In-Reply-To'] = '<'.$last_customer_thread->getMessageId().'>';
//$headers['References'] = '<'.$last_customer_thread->message_id.'>';
// https://github.com/freescout-helpdesk/freescout/issues/3175
$i = 0;
$references_array = [];
foreach ($this->threads as $thread) {
if ($i > 0) {
$reference = $thread->getMessageId();
if ($reference) {
$references_array[] = $reference;
}
}
$i++;
}
if ($references_array) {
// Trim the list of References: some SMTP relays reject messages
// whose headers block exceeds 4096 chars.
// Preserve the first and the last reference.
// https://github.com/freescout-help-desk/freescout/issues/5542
$max_references_length = 1500;
$recent = [];
$recent[] = $references_array[0];
$length = strlen($references_array[0]) + 3;
foreach ($references_array as $i => $reference) {
// The first and the last Reference are added manually.
if ($i == 0 || $i == count($references_array)-1) {
continue;
}
$length += strlen($reference) + 3;
if ($length > $max_references_length) {
break;
}
$recent[] = $reference;
}
// Append the last.
if (!empty($references_array[count($references_array)-1])) {
$recent[] = $references_array[count($references_array)-1];
}
// Oldest first, per RFC 5322 3.6.4.
$references_array = array_reverse($recent);
$references = '<'.implode('> <', $references_array).'>';
}
if ($references) {
$headers['References'] = $references;
}
}
// For Outlook threading.
// https://github.com/freescout-help-desk/freescout/issues/4922
if ($last_customer_thread) {
$outloook_thread_index = $last_customer_thread->getHeader('Thread-Index');
if (!empty($outloook_thread_index)) {
// A reply must extend the parent's conversation index with
// a 5-byte child block (MS-OXOMSG 2.2.1.3) rather than echo
// it verbatim, otherwise Outlook files the reply as a new
// conversation and the customer's original message is left
// on its own. Falls back to the verbatim copy if the value
// does not decode as a conversation index.
$headers['Thread-Index'] = self::extendThreadIndex($outloook_thread_index);
$outloook_thread_topic = $last_customer_thread->getHeader('Thread-Topic');
if ($outloook_thread_topic) {
$headers['Thread-Topic'] = $outloook_thread_topic;
}
}
}
// Conversation history.
$email_conv_history = config('app.email_conv_history');
@@ -174,19 +280,41 @@ class SendReplyToCustomer implements ShouldQueue
}
// Configure mail driver according to Mailbox settings
\App\Misc\Mail::setMailDriver($mailbox, $this->last_thread->created_by_user, $this->conversation);
\MailHelper::setMailDriver($mailbox, $this->last_thread->created_by_user, $this->conversation, $this->last_thread);
// Get penultimate email Message-Id if reply
if (!$new && !empty($last_customer_thread) && $last_customer_thread->message_id) {
$headers['In-Reply-To'] = '<'.$last_customer_thread->message_id.'>';
$headers['References'] = '<'.$last_customer_thread->message_id.'>';
// https://github.com/freescout-helpdesk/freescout/issues/3330
if (!\MailHelper::$smtp_queue_id_plugin_registered) {
\Mail::getSwiftMailer()->registerPlugin(new SwiftGetSmtpQueueId());
\MailHelper::$smtp_queue_id_plugin_registered = true;
}
$this->message_id = \App\Misc\Mail::MESSAGE_ID_PREFIX_REPLY_TO_CUSTOMER.'-'.$this->last_thread->id.'-'.\MailHelper::getMessageIdHash($this->last_thread->id).'@'.$mailbox->getEmailDomain();
$this->message_id = $this->last_thread->getMessageId($mailbox);
$headers['Message-ID'] = $this->message_id;
$this->customer_email = $this->conversation->customer_email;
// https://github.com/freescout-help-desk/freescout/issues/5121
if ($this->customer->id == $this->conversation->customer_id) {
$this->customer_email = $this->conversation->customer_email;
} else {
$this->customer_email = $this->last_thread->getToArray()[0] ?: $this->conversation->customer_email;
}
// For phone conversations we may need to get customer email.
// https://github.com/freescout-helpdesk/freescout/issues/3270
if (!$this->customer_email && $this->conversation->isPhone()) {
$this->customer_email = $this->conversation->customer->getMainEmail();
if (!$this->customer_email) {
return;
}
}
// Try to get customer by email
if (!$this->customer) {
$this->customer = Customer::getByEmail($this->customer_email);
if (!$this->customer) {
return;
}
}
$to_array = $mailbox->removeMailboxEmailsFromList($this->last_thread->getToArray());
$cc_array = $mailbox->removeMailboxEmailsFromList($this->last_thread->getCcArray());
$bcc_array = $mailbox->removeMailboxEmailsFromList($this->last_thread->getBccArray());
@@ -227,17 +355,25 @@ class SendReplyToCustomer implements ShouldQueue
if (!$new && !$is_forward) {
$subject = 'Re: '.$subject;
}
$subject = \Eventy::filter('email.reply_to_customer.subject', $subject, $this->conversation);
$subject = \Eventy::filter('email.reply_to_customer.subject', $subject, $this->conversation, $this->last_thread);
$this->threads = \Eventy::filter('email.reply_to_customer.threads', $this->threads, $this->conversation, $mailbox);
$headers['X-FreeScout-Mail-Type'] = 'customer.message';
$reply_mail = new ReplyToCustomer($this->conversation, $this->threads, $headers, $mailbox, $subject, $threads_count);
$reply_mail = new ReplyToCustomer($this->conversation, $this->threads, $headers, $mailbox, $subject, $threads_count, $this->mailbox_change_history);
$smtp_queue_id = null;
try {
Mail::to($to)
->cc($cc_array)
->bcc($bcc_array)
->send($reply_mail);
$this->last_thread->send_status = SendLog::STATUS_ACCEPTED;
$this->last_thread->save();
$smtp_queue_id = SwiftGetSmtpQueueId::$last_smtp_queue_id;
} catch (\Exception $e) {
// We come here in case SMTP server unavailable for example
if ($this->attempts() == 1) {
@@ -245,6 +381,8 @@ class SendReplyToCustomer implements ShouldQueue
->causedBy($this->customer)
->withProperties([
'error' => $e->getMessage().'; File: '.$e->getFile().' ('.$e->getLine().')',
'to' => $this->customer_email,
'conversation' => '#'.$this->conversation->number.'-'.$this->last_thread->id,
])
->useLog(\App\ActivityLog::NAME_EMAILS_SENDING)
->log(\App\ActivityLog::DESCRIPTION_EMAILS_SENDING_ERROR_TO_CUSTOMER);
@@ -261,6 +399,14 @@ class SendReplyToCustomer implements ShouldQueue
$error_message = $e->getMessage();
// Remove stack trace from error message.
$error_message = preg_replace('#[\r\n]*" in /.*#s', '"', $error_message);
// SMTP response code is stored in the exception message:
// Expected response code 235 but got code "535", with message...
preg_match('#but got code "(\d+)",#', $error_message, $response_m);
$response_code = (int)($response_m[1] ?? 0);
// Retry job with delay.
// https://stackoverflow.com/questions/35258175/how-can-i-create-delays-between-failed-queued-job-attempts-in-laravel
if ($this->attempts() < $this->tries && !preg_match("/".config("app.no_retry_mail_errors")."/i", $error_message)) {
@@ -272,6 +418,13 @@ class SendReplyToCustomer implements ShouldQueue
$this->release(3600);
}
// If an email has not been sent after 1 hour - show an error message to support agent.
if ($this->attempts() >= 3 || $response_code >= 500) {
$this->last_thread->send_status = SendLog::STATUS_SEND_INTERMEDIATE_ERROR;
$this->last_thread->updateSendStatusData(['msg' => $error_message]);
$this->last_thread->save();
}
throw $e;
} else {
$this->last_thread->send_status = SendLog::STATUS_SEND_ERROR;
@@ -285,78 +438,108 @@ class SendReplyToCustomer implements ShouldQueue
}
}
SwiftGetSmtpQueueId::$last_smtp_queue_id = null;
// Clean error message if email finally has been sent.
if ($this->last_thread->send_status == SendLog::STATUS_SEND_ERROR) {
$this->last_thread->send_status = null;
$this->last_thread->updateSendStatusData(['msg' => '']);
$this->last_thread->save();
}
$imap_sent_folder = $mailbox->imap_sent_folder;
if ($imap_sent_folder) {
if ($imap_sent_folder && \MailHelper::$smtp_mime_message) {
try {
$client = \MailHelper::getMailboxClient($mailbox);
$client->connect();
$envelope['from'] = $mailbox->getMailFrom(null, $this->conversation)['address'];
$envelope['to'] = $this->customer_email;
$envelope['subject'] = $subject;
$envelope['date'] = now()->toRfc2822String();
$envelope['message_id'] = $this->message_id;
// $mail_from = $mailbox->getMailFrom($this->last_thread->created_by_user ?? null, $this->conversation);
// Get penultimate email Message-Id if reply
if (!$new && !empty($last_customer_thread) && $last_customer_thread->message_id) {
$envelope['custom_headers'] = [
'In-Reply-To: <'.$last_customer_thread->message_id.'>',
'References: <'.$last_customer_thread->message_id.'>'
];
}
// Remove new lines to avoid "imap_mail_compose(): header injection attempt in subject".
foreach ($envelope as $i => $envelope_value) {
$envelope[$i] = preg_replace("/[\r\n]/", '', $envelope_value);
}
// if (!empty($mail_from['name'])) {
// $envelope['from'] = '"'.$mail_from['name'].'" <'.$mail_from['address'].'>';
// } else {
// $envelope['from'] = $mail_from['address'];
// }
// if (is_array($to) && !empty($to[0]) && !empty($to[0]['name']) && !empty($to[0]['email'])) {
// $envelope['to'] = '"'.$to[0]['name'].'" <'.$to[0]['email'].'>';
// } else {
// $envelope['to'] = $this->customer_email;
// }
// $envelope['subject'] = $subject;
// $envelope['date'] = now()->toRfc2822String();
// $envelope['message_id'] = $this->message_id;
$parts = [];
// // CC.
// if (count($cc_array)) {
// $envelope['cc'] = implode(',', $cc_array);
// }
// Multipart flag.
if ($this->last_thread->has_attachments) {
$multipart = [];
$multipart["type"] = TYPEMULTIPART;
$multipart["subtype"] = "alternative";
$parts[] = $multipart;
}
// // Get penultimate email Message-Id if reply
// if (!$new && !empty($last_customer_thread) && $last_customer_thread->message_id) {
// $envelope['custom_headers'] = [
// 'In-Reply-To: <'.$last_customer_thread->message_id.'>',
// 'References: '.$references,
// ];
// }
// // Remove new lines to avoid "imap_mail_compose(): header injection attempt in subject".
// foreach ($envelope as $i => $envelope_value) {
// $envelope[$i] = preg_replace("/[\r\n]/", '', $envelope_value);
// }
// Body.
$part_body['type'] = TYPETEXT;
$part_body['subtype'] = 'html';
$part_body['contents.data'] = $reply_mail->render();
$part_body['charset'] = 'utf-8';
// $parts = [];
$parts[] = $part_body;
// // Multipart flag.
// if ($this->last_thread->has_attachments) {
// $multipart = [];
// $multipart["type"] = \Webklex\PHPIMAP\IMAP::TYPEMULTIPART;
// $multipart["subtype"] = "mixed";
// // https://github.com/freescout-helpdesk/freescout/issues/3934
// //$multipart["subtype"] = "alternative";
// $parts[] = $multipart;
// }
// Add attachments.
if ($this->last_thread->has_attachments) {
// // Body.
// $part_body['type'] = \Webklex\PHPIMAP\IMAP::TYPETEXT;
// $part_body['subtype'] = 'html';
// $part_body['contents.data'] = $reply_mail->render();
// $part_body['charset'] = 'utf-8';
foreach ($this->last_thread->attachments as $attachment) {
// $parts[] = $part_body;
if ($attachment->embedded) {
continue;
}
// // Add attachments.
// if ($this->last_thread->has_attachments) {
if ($attachment->fileExists()) {
$part = [];
$part["type"] = 'APPLICATION';
$part["encoding"] = ENCBASE64;
$part["subtype"] = "octet-stream";
$part["description"] = $attachment->file_name;
$part['disposition.type'] = 'attachment';
$part['disposition'] = array('filename' => $attachment->file_name);
$part['type.parameters'] = array('name' => $attachment->file_name);
$part["description"] = '';
$part["contents.data"] = base64_encode($attachment->getFileContents());
// foreach ($this->last_thread->attachments as $attachment) {
// if ($attachment->embedded) {
// continue;
// }
// if ($attachment->fileExists()) {
// $part = [];
// $part["type"] = 'APPLICATION';
// $part["encoding"] = \Webklex\PHPIMAP\IMAP::ENCBASE64;
// $part["subtype"] = "octet-stream";
// $part["description"] = $attachment->file_name;
// $part['disposition.type'] = 'attachment';
// $part['disposition'] = array('filename' => $attachment->file_name);
// $part['type.parameters'] = array('name' => $attachment->file_name);
// $part["description"] = '';
// $part["contents.data"] = base64_encode($attachment->getFileContents());
$parts[] = $part;
} else {
\Log::error('[IMAP Folder To Save Outgoing Replies] Thread: '.$this->last_thread->id.'. Attachment file not find on disk: '.$attachment->getLocalFilePath());
}
}
}
// $parts[] = $part;
// } else {
// \Log::error('[IMAP Folder To Save Outgoing Replies] Thread: '.$this->last_thread->id.'. Attachment file not find on disk: '.$attachment->getLocalFilePath());
// }
// }
// }
try {
// https://github.com/freescout-helpdesk/freescout/issues/3502
$imap_sent_folder = mb_convert_encoding($imap_sent_folder, "UTF7-IMAP", "UTF-8");
// https://github.com/Webklex/php-imap/issues/380
if (method_exists($client, 'getFolderByPath')) {
$folder = $client->getFolderByPath($imap_sent_folder);
@@ -366,29 +549,35 @@ class SendReplyToCustomer implements ShouldQueue
// Get folder method does not work if sent folder has spaces.
if ($folder) {
try {
$save_result = $this->saveEmailToFolder($client, $folder, $envelope, $parts);
//$save_result = $this->saveEmailToFolder($client, $folder, $envelope, $parts, $bcc_array);
$save_result = $this->saveEmailToFolder($folder, \MailHelper::$smtp_mime_message);
\MailHelper::$smtp_mime_message = '';
// Sometimes emails with attachments by some reason are not saved.
// https://github.com/freescout-helpdesk/freescout/issues/2749
// if (!$save_result) {
// // Save without attachments.
// $save_result = $this->saveEmailToFolder($client, $folder, $envelope, [$part_body], $bcc_array);
if (!$save_result) {
// Save without attachments.
$this->saveEmailToFolder($client, $folder, $envelope, [$part_body]);
\Log::error($this->getImapSaveErrorPrefix($mailbox).'Could not save outgoing reply to the IMAP folder (check folder name and make sure IMAP folder does not have spaces - folders with spaces do not work): '.$imap_sent_folder);
}
//}
} catch (\Exception $e) {
// Just log error and continue.
\Helper::logException($e, 'Could not save outgoing reply to the IMAP folder: ');
\Helper::logException($e, $this->getImapSaveErrorPrefix($mailbox).'Could not save outgoing reply to the IMAP folder: ');
}
} else {
\Log::error('Could not save outgoing reply to the IMAP folder (make sure IMAP folder does not have spaces - folders with spaces do not work): '.$imap_sent_folder);
\Log::error($this->getImapSaveErrorPrefix($mailbox).'Could not save outgoing reply to the IMAP folder (check folder name and make sure IMAP folder does not have spaces - folders with spaces do not work): '.$imap_sent_folder);
}
} catch (\Exception $e) {
// Just log error and continue.
\Helper::logException($e, 'Could not save outgoing reply to the IMAP folder, IMAP folder not found: '.$imap_sent_folder.' - ');
\Helper::logException($e, $this->getImapSaveErrorPrefix($mailbox).'Could not save outgoing reply to the IMAP folder, IMAP folder not found: '.$imap_sent_folder.' - ');
//$this->saveToSendLog('['.date('Y-m-d H:i:s').'] Could not save outgoing reply to the IMAP folder: '.$imap_sent_folder);
}
} catch (\Exception $e) {
// Just log error and continue.
//$this->saveToSendLog('['.date('Y-m-d H:i:s').'] Could not get mailbox IMAP folder: '.$imap_sent_folder);
\Helper::logException($e, 'Could not save outgoing reply to the IMAP folder: '.$imap_sent_folder.' - ');
\Helper::logException($e, $this->getImapSaveErrorPrefix($mailbox).'Could not save outgoing reply to the IMAP folder: '.$imap_sent_folder.' - ');
}
}
@@ -401,19 +590,42 @@ class SendReplyToCustomer implements ShouldQueue
$this->failures = Mail::failures();
// Save to send log
$this->saveToSendLog();
$this->saveToSendLog('', $smtp_queue_id);
}
public function getImapSaveErrorPrefix($mailbox)
{
return '['.$mailbox->name.' » Connection Settings » Fetching Emails » IMAP Folder To Save Outgoing Replies] ';
}
// Save an email to IMAP folder.
public function saveEmailToFolder($client, $folder, $envelope, $parts)
public function saveEmailToFolder($folder, $message_str)
{
if (get_class($client) == 'Webklex\PHPIMAP\Client') {
return $folder->appendMessage(imap_mail_compose($envelope, $parts), ['Seen'], now()->format('d-M-Y H:i:s O'));
} else {
return $folder->appendMessage(imap_mail_compose($envelope, $parts), '\Seen', now()->format('d-M-Y H:i:s O'));
}
return $folder->appendMessage($message_str, ['\Seen'], now()->format('d-M-Y H:i:s O'));
}
// public function saveEmailToFolder($client, $folder, $envelope, $parts, $bcc = [])
// {
// $envelope_str = imap_mail_compose($envelope, $parts);
// // Add BCC.
// // https://stackoverflow.com/questions/47353938/php-imap-append-with-bcc
// if (!empty($bcc)) {
// // There will be a "To:" parameter for sure.
// $to_pos = strpos($envelope_str , "To:");
// if ($to_pos !== false) {
// $bcc_str = "Bcc: " . implode(',', $bcc) . "\r\n";
// $envelope_str = substr_replace($envelope_str , $bcc_str, $to_pos, 0);
// }
// }
// if (get_class($client) == 'Webklex\PHPIMAP\Client') {
// return $folder->appendMessage($envelope_str, ['\Seen'], now()->format('d-M-Y H:i:s O'));
// } else {
// return $folder->appendMessage($envelope_str, '\Seen', now()->format('d-M-Y H:i:s O'));
// }
// }
/**
* The job failed to process.
* This method is called after attempts had finished.
@@ -425,11 +637,18 @@ class SendReplyToCustomer implements ShouldQueue
*/
public function failed(\Exception $e)
{
// failed() is called by unserializing the original queue payload.
if (!$this->last_thread && $this->threads && count($this->threads)) {
$this->threads = Thread::sortThreads($this->threads);
$this->last_thread = $this->threads->first();
}
activity()
->causedBy($this->customer)
->withProperties([
'error' => $e->getMessage().'; File: '.$e->getFile().' ('.$e->getLine().')',
'to' => $this->customer_email,
'conversation' => '#'.$this->conversation->number.'-'.($this->last_thread ? $this->last_thread->id : ''),
])
->useLog(\App\ActivityLog::NAME_EMAILS_SENDING)
->log(\App\ActivityLog::DESCRIPTION_EMAILS_SENDING_ERROR_TO_CUSTOMER);
@@ -440,7 +659,7 @@ class SendReplyToCustomer implements ShouldQueue
/**
* Save emails to send log.
*/
public function saveToSendLog($error_message = '')
public function saveToSendLog($error_message = '', $smtp_queue_id = '')
{
foreach ($this->recipients as $recipient) {
if (in_array($recipient, $this->failures)) {
@@ -455,7 +674,44 @@ class SendReplyToCustomer implements ShouldQueue
} else {
$customer_id = null;
}
SendLog::log($this->last_thread->id, $this->message_id, $recipient, SendLog::MAIL_TYPE_EMAIL_TO_CUSTOMER, $status, $customer_id, null, $status_message);
SendLog::log($this->last_thread->id, $this->message_id, $recipient, SendLog::MAIL_TYPE_EMAIL_TO_CUSTOMER, $status, $customer_id, null, $status_message, $smtp_queue_id);
}
}
/**
* Append a 5-byte child block to an Outlook conversation index
* (Thread-Index header, MS-OXOMSG 2.2.1.3) so the reply nests under the
* customer's message instead of being read by Outlook as a new
* conversation. Header = 0x01 + 5 bytes of FILETIME (bits 16-55) + 16-byte
* GUID; each child = code(1 bit) + time delta(31) + random(4) + sequence(4).
* Returns the input unchanged when it does not decode as a valid index.
* https://github.com/freescout-help-desk/freescout/issues/4922
*/
public static function extendThreadIndex($thread_index)
{
$raw = base64_decode(trim((string)$thread_index), true);
if ($raw === false || strlen($raw) < 22 || (strlen($raw) - 22) % 5 !== 0) {
return $thread_index;
}
// Header timestamp: FILETIME bits 16-55, big-endian, in bytes 1-5.
$hdr = unpack('Nhi/Clo', substr($raw, 1, 5));
$hdr_bits = ($hdr['hi'] << 8) | $hdr['lo'];
$now_ft = (int) round((microtime(true) + 11644473600) * 10000000);
$now_bits = ($now_ft >> 16) & 0xFFFFFFFFFF;
$delta = ($now_bits - $hdr_bits) << 16;
if ($delta < 0) {
$delta = 0;
}
$code = 0;
$diff = $delta >> 18;
if ($diff > 0x7FFFFFFF) {
$code = 1;
$diff = min($delta >> 23, 0x7FFFFFFF);
}
// 40-bit child block: code(1) | diff(31) | random(4) | sequence(4)=0
$block = (($code << 39) | ($diff << 8) | (random_int(0, 15) << 4)) & 0xFFFFFFFFFF;
$bytes = pack('C5', ($block >> 32) & 0xFF, ($block >> 24) & 0xFF, ($block >> 16) & 0xFF, ($block >> 8) & 0xFF, $block & 0xFF);
return base64_encode($raw.$bytes);
}
}

View File

@@ -2,9 +2,8 @@
namespace App\Jobs;
use App\Conversation;
use App\ConversationFolder;
use App\Folder;
use Illuminate\Support\Facades\Cache;
use Illuminate\Bus\Queueable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
@@ -15,28 +14,33 @@ class UpdateFolderCounters implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
/**
* @var Folder
*/
private $folder;
/**
* Create a new job instance.
*
* @return void
*/
// Cache lock key
private $lockKey;
public function __construct(Folder $folder)
{
$this->folder = $folder;
$this->lockKey = "folder_update_lock_{$folder->id}";
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
$this->folder->updateCountersNow();
// Check if another job is processing this folder
if (Cache::has($this->lockKey)) {
return; // Exit if already processing
}
// Set lock with a timeout of 5 minutes (adjust as needed)
Cache::put($this->lockKey, true, now()->addMinutes(5));
try {
// Perform the folder update logic
$this->folder->updateCountersNow();
} finally {
// Release the lock after processing
Cache::forget($this->lockKey);
}
}
}
}

View File

@@ -25,6 +25,8 @@ class LogFailedLogin
*/
public function handle(Failed $event)
{
\Eventy::action('listeners.failed_login', $event);
activity()
//->causedBy($event->user)
->withProperties(['ip' => app('request')->ip(), 'email' => request()->email])

View File

@@ -25,6 +25,9 @@ class RefreshConversations
*/
public function handle($event)
{
Conversation::refreshConversations($event->conversation, $event->thread ?? $event->last_thread);
$thread = $event->thread ?? $event->last_thread;
if ($thread) {
Conversation::refreshConversations($event->conversation, $thread);
}
}
}

View File

@@ -44,7 +44,7 @@ class SendAutoReply
}
// Do not send auto replies to spam messages.
if ($conversation->status == Conversation::STATUS_SPAM) {
if ($conversation->isSpam()) {
return;
}

View File

@@ -26,6 +26,7 @@ class SendNotificationToUsers
{
$event_type = null;
$caused_by_user_id = null;
$extra_data = [];
// Detect event type by event class
switch (get_class($event)) {
@@ -47,7 +48,7 @@ class SendNotificationToUsers
break;
case 'App\Events\CustomerCreatedConversation':
// Do not send notification if conversation is spam.
if ($event->conversation->status != Conversation::STATUS_SPAM) {
if (!$event->conversation->isSpam()) {
$event_type = Subscription::EVENT_TYPE_NEW;
}
break;
@@ -55,7 +56,17 @@ class SendNotificationToUsers
$caused_by_user_id = $event->user->id;
$event_type = Subscription::EVENT_TYPE_ASSIGNED;
break;
case 'App\Events\UserMovedConversation':
$caused_by_user_id = $event->caused_by_user_id;
$extra_data['from_mailbox'] = $event->from_mailbox;
$event_type = Subscription::EVENT_TYPE_USER_MOVED;
break;
case 'App\Events\CustomerReplied':
// Do not send notifications to users if customer sent a reply
// to the conversation marked as Spam.
if (!empty($event->conversation) && $event->conversation->isSpam()) {
return;
}
$event_type = Subscription::EVENT_TYPE_CUSTOMER_REPLIED;
break;
}
@@ -70,6 +81,6 @@ class SendNotificationToUsers
$conversation = $event->conversation;
// Using the last argument you can make event to be processed immediately
Subscription::registerEvent($event_type, $conversation, $caused_by_user_id/*, true*/);
Subscription::registerEvent($event_type, $conversation, $caused_by_user_id, $extra_data);
}
}

View File

@@ -3,6 +3,8 @@
namespace App\Listeners;
use App\Conversation;
use App\Customer;
use App\Thread;
class SendReplyToCustomer
{
@@ -23,12 +25,20 @@ class SendReplyToCustomer
{
$conversation = $event->conversation;
// Do not send email if this is a Phone conversation.
$main_customer_email = $conversation->customer->getMainEmail();
// Do not send email if this is a Phone conversation and customer has no email.
if ($conversation->isPhone()) {
return;
if (!$main_customer_email) {
return;
}
}
$replies = $conversation->getReplies();
// Get threads with line items, line items are needed
// to show proper signature when conversation is being moved
// between mailboxes.
//$replies = $conversation->getReplies();
$replies = $conversation->getThreads(null, null, [Thread::TYPE_CUSTOMER, Thread::TYPE_MESSAGE, Thread::TYPE_LINEITEM]);
// Ignore imported messages.
if ($replies && $replies->first() && $replies->first()->imported) {
@@ -45,6 +55,33 @@ class SendReplyToCustomer
$replies->forget($i);
}
}
} else {
// If thread is not provided - stop.
// It allows to trigger event but avoid sending an email to customer.
return;
}
// In order to show proper signature.
// https://github.com/freescout-help-desk/freescout/issues/5419
$mailbox_change_history = [];
$new_mailbox_id = null;
foreach ($replies as $i => $reply) {
if ($reply->action_type == Thread::ACTION_TYPE_MOVED_FROM_MAILBOX && is_numeric($reply->action_data)) {
$new_mailbox_id = (int)$reply->action_data;
}
if ($reply->isReply() && $new_mailbox_id !== null) {
$mailbox_change_history[$reply->id] = $new_mailbox_id;
$new_mailbox_id = null;
}
}
// Now remove line items.
$replies = $replies->whereIn('type', [Thread::TYPE_CUSTOMER, Thread::TYPE_MESSAGE]);
// Allow to cancel mail sending if needed.
$skip_send = \Eventy::filter('conversation.skip_send_reply_to_customer', false, $conversation, $replies);
if ($skip_send) {
return;
}
// Chat conversation.
@@ -58,7 +95,23 @@ class SendReplyToCustomer
//if (!$conversation->imported) {
$delay = \Eventy::filter('conversation.send_reply_to_customer_delay', now()->addSeconds(Conversation::UNDO_TIMOUT), $conversation, $replies);
\App\Jobs\SendReplyToCustomer::dispatch($conversation, $replies, $conversation->customer)
$recipient_customer = $conversation->customer;
// The reply may be sent to some other customer from previous threads.
// https://github.com/freescout-help-desk/freescout/pull/5199
if ($thread
&& ($to_array = $thread->getToArray())
&& !empty($to_array[0])
&& ($customer_email = $to_array[0])
&& $customer_email != $main_customer_email
) {
$other_customer = Customer::getByEmail($customer_email);
if ($other_customer) {
$recipient_customer = $other_customer;
}
}
\App\Jobs\SendReplyToCustomer::dispatch($conversation, $replies, $recipient_customer)
->delay($delay)
->onQueue('emails');
}

View File

@@ -26,15 +26,21 @@ class AutoReply extends Mailable
*/
public $headers = [];
/**
* Mailbox alias to send From (empty to use mailbox default email).
*/
public $from_alias = '';
/**
* Create a new message instance.
*/
public function __construct($conversation, $mailbox, $customer, $headers)
public function __construct($conversation, $mailbox, $customer, $headers, $from_alias = '')
{
$this->conversation = $conversation;
$this->mailbox = $mailbox;
$this->customer = $customer;
$this->headers = $headers;
$this->from_alias = $from_alias;
}
/**
@@ -77,15 +83,54 @@ class AutoReply extends Mailable
public function setHeaders()
{
$new_headers = $this->headers;
if (!empty($new_headers)) {
$this->withSwiftMessage(function ($swiftmessage) use ($new_headers) {
if (!empty($new_headers['Message-ID'])) {
$swiftmessage->setId($new_headers['Message-ID']);
$from_alias = $this->from_alias;
if (!empty($new_headers) || $from_alias) {
$mailbox = $this->mailbox;
$conversation = $this->conversation;
$this->withSwiftMessage(function ($swiftmessage) use ($new_headers, $from_alias, $mailbox, $conversation) {
$headers = null;
if (!empty($new_headers)) {
if (!empty($new_headers['Message-ID'])) {
$swiftmessage->setId($new_headers['Message-ID']);
}
$headers = $swiftmessage->getHeaders();
foreach ($new_headers as $header => $value) {
if ($header != 'Message-ID') {
$headers->addTextHeader($header, $value);
}
}
}
$headers = $swiftmessage->getHeaders();
foreach ($new_headers as $header => $value) {
if ($header != 'Message-ID') {
$headers->addTextHeader($header, $value);
if (!empty($from_alias)) {
$aliases = $mailbox->getAliases();
if (array_key_exists($from_alias, $aliases)) {
$from_alias_name = $aliases[$from_alias] ?? '';
// Take into account mailbox From Name setting.
$mailbox_mail_from = $mailbox->getMailFrom(null, $conversation);
if ($mailbox_mail_from['name'] == $mailbox->name && $from_alias_name) {
// Use name from alias.
} else {
$from_alias_name = $mailbox_mail_from['name'];
}
if (!$headers) {
$headers = $swiftmessage->getHeaders();
}
$swift_from = $headers->get('From');
if ($from_alias_name) {
$swift_from->setNameAddresses([
$from_alias => $from_alias_name,
]);
} else {
$swift_from->setAddresses([
$from_alias,
]);
}
}
}

View File

@@ -48,12 +48,17 @@ class ReplyToCustomer extends Mailable
*/
public $threads_count;
/**
* Needed to show proper signature when conversation is being moved between mailboxes.
*/
public $mailbox_change_history;
/**
* Create a new message instance.
*
* @return void
*/
public function __construct($conversation, $threads, $headers, $mailbox, $subject, $threads_count = 1)
public function __construct($conversation, $threads, $headers, $mailbox, $subject, $threads_count = 1, $mailbox_change_history = [])
{
$this->conversation = $conversation;
$this->threads = $threads;
@@ -61,6 +66,7 @@ class ReplyToCustomer extends Mailable
$this->mailbox = $mailbox;
$this->subject = $subject;
$this->threads_count = $threads_count;
$this->mailbox_change_history = $mailbox_change_history;
}
/**
@@ -81,6 +87,10 @@ class ReplyToCustomer extends Mailable
if (!empty($new_headers) || $from_alias) {
$mailbox = $this->mailbox;
$this->withSwiftMessage(function ($swiftmessage) use ($new_headers, $from_alias, $mailbox, $thread) {
\MailHelper::$smtp_mime_message = '';
$headers = null;
if (!empty($new_headers)) {
if (!empty($new_headers['Message-ID'])) {
$swiftmessage->setId($new_headers['Message-ID']);
@@ -111,36 +121,53 @@ class ReplyToCustomer extends Mailable
$from_alias_name = $mailbox_mail_from['name'];
}
if (!$headers) {
$headers = $swiftmessage->getHeaders();
}
$swift_from = $headers->get('From');
if ($from_alias_name) {
$swift_from->setNameAddresses([
$from_alias => $from_alias_name
$from_alias => $from_alias_name,
]);
} else {
$swift_from->setAddresses([
$from_alias
$from_alias,
]);
}
}
}
\Eventy::action('email.reply_to_customer.swiftmessage', $swiftmessage, $from_alias, $thread, $mailbox);
if ($mailbox->imap_sent_folder) {
\MailHelper::$smtp_mime_message = $swiftmessage->toString();
}
return $swiftmessage;
});
}
$template_html = \Eventy::filter('email.reply_to_customer.template_name_html', 'emails/customer/reply_fancy');
$template_text = \Eventy::filter('email.reply_to_customer.template_name_text', 'emails/customer/reply_fancy_text');
// from($this->from) Sets only email, name stays empty.
// So we set from in Mail::setMailDriver
$message = $this->subject($this->subject)
->view('emails/customer/reply_fancy')
->text('emails/customer/reply_fancy_text');
->view($template_html)
->text($template_text);
if ($thread->has_attachments) {
foreach ($thread->attachments as $attachment) {
if ($attachment->fileExists()) {
$message->attach($attachment->getLocalFilePath());
if (\Helper::isLocalStorage()) {
if ($attachment->fileExists()) {
$message->attach($attachment->getLocalFilePath());
} else {
\Log::error('[ReplyToCustomer] Thread: '.$thread->id.'. Attachment file not find on disk: '.$attachment->getLocalFilePath());
}
} else {
\Log::error('[ReplyToCustomer] Thread: '.$thread->id.'. Attachment file not find on disk: '.$attachment->getLocalFilePath());
\Eventy::filter('email.reply_to_customer.attach', false, $message, $attachment, $thread->id);
}
}
}

View File

@@ -9,13 +9,16 @@ use Illuminate\Mail\Mailable;
class UserEmailReplyError extends Mailable
{
public $text;
/**
* Create a new message instance.
*
* @return void
*/
public function __construct()
public function __construct($text = null)
{
$this->text = $text;
}
/**

View File

@@ -91,15 +91,20 @@ class UserNotification extends Mailable
});
}
$subject = '[#'.$this->conversation->number.'] '.$this->conversation->subject;
$subject = \Eventy::filter('email.user_notification.subject', '[#' . (isset($this->conversation->number) ? $this->conversation->number : '') . '] ' . $this->conversation->subject, $this->conversation);
$customer = $this->conversation->customer;
$thread = $this->threads->first();
$template_html = \Eventy::filter('email.user_notification.template_name_html', 'emails/user/notification');
$template_text = \Eventy::filter('email.user_notification.template_name_text', 'emails/user/notification_text');
$template_fields = \Eventy::filter('email.user_notification.template_fields', ['customer' => $customer, 'thread' => $thread, 'mailbox' => $this->mailbox]);
return $this->subject($subject)
->from($this->from['address'], $this->from['name'])
->view('emails/user/notification', ['customer' => $customer, 'thread' => $thread, 'mailbox' => $this->mailbox])
->text('emails/user/notification_text', ['customer' => $customer, 'thread' => $thread, 'mailbox' => $this->mailbox]);
->view($template_html, $template_fields)
->text($template_text, $template_fields);
}
}

View File

@@ -3,6 +3,7 @@
namespace App;
use App\Email;
use App\Thread;
use App\User;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Hash;
@@ -14,6 +15,12 @@ class Mailbox extends Model
// This is obligatory.
public $rememberCacheDriver = 'array';
/**
* State.
*/
const STATE_ACTIVE = 1;
const STATE_ARCHIVED = 2;
/**
* From Name: name that will appear in the From field when a customer views your email.
*/
@@ -58,6 +65,9 @@ class Mailbox extends Model
const OUT_ENCRYPTION_NONE = 1;
const OUT_ENCRYPTION_SSL = 2;
const OUT_ENCRYPTION_TLS = 3;
// StartTLS is already included in TLS:
// allow_self_signed = true
// verify_peer = false
public static $out_encryptions = [
self::OUT_ENCRYPTION_NONE => '',
@@ -82,11 +92,16 @@ class Mailbox extends Model
const IN_ENCRYPTION_NONE = 1;
const IN_ENCRYPTION_SSL = 2;
const IN_ENCRYPTION_TLS = 3;
// For Webklex/laravel-imap this option is the same as 'none'.
// For Webklex/php-imap it works:
// https://github.com/Webklex/php-imap/pull/180
const IN_ENCRYPTION_STARTTLS = 4;
public static $in_encryptions = [
self::IN_ENCRYPTION_NONE => '',
self::IN_ENCRYPTION_SSL => 'ssl',
self::IN_ENCRYPTION_TLS => 'tls',
self::IN_ENCRYPTION_STARTTLS => 'starttls',
];
/**
@@ -102,6 +117,7 @@ class Mailbox extends Model
const ACCESS_PERM_PERMISSIONS = 'perm';
const ACCESS_PERM_AUTO_REPLIES = 'auto';
const ACCESS_PERM_SIGNATURE = 'sig';
const ACCESS_PERM_ASSIGNED = 'asg';
public static $access_permissions = [
self::ACCESS_PERM_EDIT,
@@ -139,7 +155,7 @@ class Mailbox extends Model
*
* @var [type]
*/
protected $fillable = ['name', 'email', 'aliases', 'aliases_reply', 'auto_bcc', 'from_name', 'from_name_custom', 'ticket_status', 'ticket_assignee', 'template', 'before_reply', 'signature', 'out_method', 'out_server', 'out_username', 'out_password', 'out_port', 'out_encryption', 'in_server', 'in_port', 'in_username', 'in_password', 'in_protocol', 'in_encryption', 'in_validate_cert', 'auto_reply_enabled', 'auto_reply_subject', 'auto_reply_message', 'office_hours_enabled', 'ratings', 'ratings_placement', 'ratings_text', 'imap_sent_folder'];
protected $fillable = ['name', 'email', 'aliases', 'aliases_reply', 'auto_bcc', 'from_name', 'from_name_custom', 'ticket_status', 'ticket_assignee', 'before_reply', 'signature', 'out_method', 'out_server', 'out_username', 'out_password', 'out_port', 'out_encryption', 'in_server', 'in_port', 'in_username', 'in_password', 'in_protocol', 'in_encryption', 'in_validate_cert', 'auto_reply_enabled', 'auto_reply_subject', 'auto_reply_message', 'office_hours_enabled', 'ratings', 'ratings_placement', 'ratings_text', 'imap_sent_folder'];
protected static function boot()
{
@@ -208,6 +224,23 @@ class Mailbox extends Model
}
}
/**
* Get mailbox state: stored in meta for now.
*/
public function getStateAttribute()
{
return $this->getMeta('st', self::STATE_ACTIVE);
}
public function setStateAttribute($value)
{
$value = (int)$value;
if (!in_array($value, [self::STATE_ACTIVE, self::STATE_ARCHIVED])) {
$value = self::STATE_ACTIVE;
}
$this->setMeta('st', $value);
}
/**
* Get users having access to the mailbox.
*/
@@ -393,12 +426,22 @@ class Mailbox extends Model
}
}
public function isActive()
{
return $this->state == self::STATE_ACTIVE;
}
public function isArchived()
{
return $this->state == self::STATE_ARCHIVED;
}
/**
* Is mailbox available for using.
*
* @return bool
*/
public function isActive()
public function isConnected()
{
return $this->isInActive() && $this->isOutActive();
}
@@ -534,6 +577,9 @@ class Mailbox extends Model
$user = User::find($user_id);
}
}
if (!$user->isAdmin() && !$this->isActive()) {
return false;
}
$filter = \Eventy::filter('mailbox.user_has_access', -1, $this, $user);
if ($filter != -1) {
return (bool)$filter;
@@ -552,18 +598,27 @@ class Mailbox extends Model
*
* @return array
*/
public function getMailFrom($from_user = null, $conversation = null)
public function getMailFrom($from_user = null, $conversation = null, $thread = null)
{
// Mailbox name by default
$name = $this->name;
if ($this->from_name == self::FROM_NAME_CUSTOM && $this->from_name_custom) {
$name = $this->from_name_custom;
$data = [
'mailbox' => $this,
'mailbox_from_name' => '', // To avoid recursion.
'conversation' => $conversation,
'user' => $from_user ?: auth()->user(),
];
$name = \MailHelper::replaceMailVars($this->from_name_custom, $data, false, true);
} elseif ($this->from_name == self::FROM_NAME_USER && $from_user) {
$name = $from_user->getFullName();
}
return [ 'address' => \Eventy::filter( 'mailbox.get_mail_from_address', $this->email, $from_user, $conversation ), 'name' => \Eventy::filter( 'mailbox.get_mail_from_name', $name, $from_user, $conversation ) ];
return [
'address' => \Eventy::filter('mailbox.get_mail_from_address', $this->email, $from_user, $conversation, $thread, $this),
'name' => \Eventy::filter('mailbox.get_mail_from_name', $name, $from_user, $conversation, $thread, $this)
];
}
/**
@@ -651,6 +706,22 @@ class Mailbox extends Model
return \Eventy::filter('mailbox.in_protocols', self::$in_protocols);
}
/**
* Determines if Fetching settings for the mailbox have been saved by admin.
*/
public function inSettingsSaved()
{
return ($this->attributes['in_password'] !== null);
}
/**
* Determines if Sending settings for the mailbox have been saved by admin.
*/
public function outSettingsSaved()
{
return ($this->attributes['out_password'] !== null);
}
/**
* Get pivot table parameters for the user.
*/
@@ -666,7 +737,8 @@ class Mailbox extends Model
}
/**
* Create dummy object with default parameters
* Create dummy object with default parameters.
*
* @return [type] [description]
*/
public static function getDummySettings()
@@ -775,7 +847,7 @@ class Mailbox extends Model
}
/**
* Get all active mailboxes.
* Get all active & connected mailboxes.
*
* @return [type] [description]
*/
@@ -786,7 +858,7 @@ class Mailbox extends Model
// It is more effective to retrive all mailboxes and filter them in PHP.
$mailboxes = self::all();
foreach ($mailboxes as $mailbox) {
if ($mailbox->isActive()) {
if ($mailbox->isActive() && $mailbox->isConnected()) {
$active[] = $mailbox;
}
}
@@ -912,6 +984,15 @@ class Mailbox extends Model
public function setMetaParam($param, $value, $save = false)
{
// Encrypt some values.
if ($param == 'oauth' && is_array($value)) {
if (!empty($value['a_token'])) {
$value['a_token'] = \Helper::encrypt($value['a_token']);
}
if (!empty($value['r_token'])) {
$value['r_token'] = \Helper::encrypt($value['r_token']);
}
}
$meta = $this->meta;
$meta[$param] = $value;
$this->meta = $meta;
@@ -956,7 +1037,84 @@ class Mailbox extends Model
public function oauthGetParam($param)
{
return $this->meta['oauth'][$param] ?? '';
$value = $this->meta['oauth'][$param] ?? '';
// Decrypt some values.
if (in_array($param, ['a_token', 'r_token'])) {
$value = \Helper::decrypt($value);
}
return $value;
}
public function inOauthEnabled()
{
return $this->oauthEnabled()
&& $this->in_username !== null
&& $this->isInUsernameOauth();
}
public function outOauthEnabled()
{
return $this->oauthEnabled()
&& $this->out_username !== null
&& $this->isOutUsernameOauth()
&& $this->out_server !== null
&& $this->isOutServerOauth();
}
// For oAuth Username may have the following format:
// test@example.org:123-456-789
public function getInOauthUsername()
{
$username = preg_replace("#:.*#", '', $this->in_username ?? '');
if (strstr($username, '@')) {
return $username;
} else {
return $this->email;
}
}
public function getInOauthClientId()
{
return preg_replace("#.*:#", '', $this->in_username ?? '');
}
public function getOutOauthUsername()
{
$username = preg_replace("#:.*#", '', $this->out_username ?? '');
if (strstr($username, '@')) {
return $username;
} else {
return $this->email;
}
}
public function getOutOauthClientId()
{
return preg_replace("#.*:#", '', $this->out_username ?? '');
}
public function isInUsernameOauth()
{
return (!strstr($this->in_username, '@') || preg_match("#.*@.*:.*#", $this->in_username));
}
public function isOutUsernameOauth()
{
return (!strstr($this->out_username, '@') || preg_match("#.*@.*:.*#", $this->out_username));
}
public function isOauthProvider($provider)
{
return ($this->oauthGetParam('provider') == $provider);
}
public function isOutServerOauth()
{
return in_array(trim($this->out_server), [\MailHelper::OAUTH_MICROSOFT_SMTP, \MailHelper::OAUTH_GOOGLE_SMTP]);
}
public function setEmailAttribute($value)
@@ -965,4 +1123,61 @@ class Mailbox extends Model
$this->attributes['email'] = Email::sanitizeEmail($value);
}
}
public function deleteMailbox()
{
// Remove threads and conversations.
$conversation_ids = $this->conversations()->pluck('id')->toArray();
// for ($i=0; $i < ceil(count($conversation_ids) / \Helper::IN_LIMIT); $i++) {
// $slice_ids = array_slice($conversation_ids, $i*\Helper::IN_LIMIT, \Helper::IN_LIMIT);
// Thread::whereIn('conversation_id', $slice_ids)->delete();
// }
// $this->conversations()->delete();
Conversation::deleteConversationsForever($conversation_ids);
$this->users()->sync([]);
$this->folders()->delete();
// Maybe remove notifications on events in this mailbox?
$this->delete();
}
// Purify signature HTML to avoid sending unsafe HTML to customers by email.
public static function sanitizeSignature($signature)
{
$signature = \Helper::purifyHtml($signature);
// Decode URL encoded variables back.
// https://github.com/freescout-help-desk/freescout/issues/5565
$signature = strtr($signature, [
'%7B%25' => '{%',
'%25%7D' => '%}',
]);
return $signature;
}
public static function excludeArchived($mailboxes)
{
if (is_array($mailboxes)) {
// Array.
$found = false;
foreach ($mailboxes as $i => $mailbox) {
if ($mailbox->isArchived()) {
$found = true;
unset($mailboxes[$i]);
}
}
if ($found) {
$mailboxes = array_values($mailboxes);
}
} else {
// Collection.
$mailboxes = $mailboxes->reject(function ($mailbox) {
return $mailbox->isArchived();
})->values();
}
return $mailboxes;
}
}

View File

@@ -20,6 +20,13 @@ class MailboxUser extends Model
public $timestamps = false;
public static $pivot_settings = [
'after_send',
'hide',
'mute',
'access',
];
// Does not work as we receive it via pivot
// protected $casts = [
// 'access' => 'array',

View File

@@ -0,0 +1,178 @@
<?php
namespace App\Misc;
class ConversationActionButtons
{
// Location constants
const LOCATION_TOOLBAR = 'toolbar';
const LOCATION_DROPDOWN = 'dropdown';
const LOCATION_BOTH = 'both';
/**
* Get all available conversation actions
*/
public static function getActions($conversation, $user, $mailbox)
{
$actions = [
// Default toolbar actions
'reply' => [
'icon' => 'glyphicon-share-alt',
'location' => self::LOCATION_TOOLBAR,
'label' => __('Reply'),
'permission' => function ($conversation) {
return ( ! $conversation->isPhone() || ( $conversation->customer && $conversation->customer->getMainEmail() ) )
&& \Eventy::filter('conversation.reply_button.enabled', true, $conversation);
},
'class' => 'conv-reply',
'fixed_location' => true,
],
'note' => [
'icon' => 'glyphicon-edit',
'location' => self::LOCATION_TOOLBAR,
'label' => __('Note'),
'permission' => function ($conversation) {
return \Eventy::filter('conversation.note_button.enabled', true, $conversation);
},
'class' => 'conv-add-note',
'fixed_location' => true,
],
'delete' => [
'icon' => 'glyphicon-trash',
'location' => self::LOCATION_TOOLBAR,
'label' => $conversation->state != \App\Conversation::STATE_DELETED ? __('Delete') : __('Delete Forever'),
'permission' => function ($conversation, $user) {
return $user->can('delete', $conversation);
},
'class' => $conversation->state != \App\Conversation::STATE_DELETED ? 'conv-delete' : 'conv-delete-forever',
'fixed_location' => true,
],
'delete_mobile' => [
'icon' => 'glyphicon-trash',
'location' => self::LOCATION_DROPDOWN,
'label' => $conversation->state != \App\Conversation::STATE_DELETED ? __('Delete') : __('Delete Forever'),
'permission' => function ($conversation, $user) {
return $user->can('delete', $conversation);
},
'class' => $conversation->state != \App\Conversation::STATE_DELETED ? 'conv-delete' : 'conv-delete-forever',
'fixed_location' => true,
'mobile_only' => true,
],
// Default dropdown actions
'follow' => [
'icon' => 'glyphicon-bell',
'location' => self::LOCATION_DROPDOWN,
'label' => __('Follow'),
'permission' => function () {
return true;
},
'class' => 'conv-follow',
'has_opposite' => true,
'opposite' => [
'label' => __('Unfollow'),
'class' => 'conv-follow',
],
'fixed_location' => true,
],
'forward' => [
'icon' => 'glyphicon-arrow-right',
'location' => self::LOCATION_DROPDOWN,
'label' => __('Forward'),
'permission' => function () {
return true;
},
'class' => 'conv-forward',
'fixed_location' => true,
],
'merge' => [
'icon' => 'glyphicon-indent-left',
'location' => self::LOCATION_DROPDOWN,
'label' => __('Merge'),
'permission' => function ($conversation) {
return ! $conversation->isChat();
},
'class' => '',
'url' => function ($conversation) {
return route('conversations.ajax_html', array_merge([ 'action' => 'merge_conv' ], \Request::all(), [ 'conversation_id' => $conversation->id ]));
},
'attrs' => [
'data-trigger' => 'modal',
'data-modal-title' => __('Merge Conversations'),
'data-modal-no-footer' => 'true',
'data-modal-on-show' => 'initMergeConv',
],
'fixed_location' => true,
],
'move' => [
'icon' => 'glyphicon-log-out',
'location' => self::LOCATION_DROPDOWN,
'label' => __('Move'),
'permission' => function ($conversation, $user) {
return $user->can('move', \App\Conversation::class);
},
'class' => '',
'url' => function ($conversation) {
return route('conversations.ajax_html', array_merge([ 'action' => 'move_conv' ], \Request::all(), [ 'conversation_id' => $conversation->id ]));
},
'attrs' => [
'data-trigger' => 'modal',
'data-modal-title' => __('Move Conversation'),
'data-modal-no-footer' => 'true',
'data-modal-on-show' => 'initMoveConv',
],
'fixed_location' => true,
],
'print' => [
'icon' => 'glyphicon-print',
'location' => self::LOCATION_DROPDOWN,
'label' => __('Print'),
'class' => '',
'permission' => function () {
return true;
},
'url' => function () {
return \Request::getRequestUri() . '&amp;print=1';
},
'attrs' => [
'target' => '_blank',
],
'fixed_location' => true,
],
];
// Allow overriding default actions while preserving backwards compatibility
$actions = \Eventy::filter('conversation.get_action_buttons', $actions, $conversation, $user, $mailbox);
// Filter actions based on permissions
foreach ($actions as $key => $action) {
if (! $action['permission']($conversation, $user)) {
unset($actions[ $key ]);
}
}
return $actions;
}
/**
* Get actions for a specific location
*/
public static function getActionsByLocation($actions, $location)
{
return array_filter($actions, function ($action) use ($location) {
if (! empty($action['fixed_location'])) {
// Modified logic for responsive display
if ($location === self::LOCATION_TOOLBAR) {
// Show in toolbar if it's a toolbar action
return $action['location'] === self::LOCATION_TOOLBAR;
} elseif ($location === self::LOCATION_DROPDOWN) {
// Show in dropdown if explicitly flagged or if it's a dropdown action
return $action['location'] === self::LOCATION_DROPDOWN ||
( ! empty($action['show_in_dropdown']) && $action['location'] === self::LOCATION_TOOLBAR );
}
}
return $action['location'] === $location;
});
}
}

47
app/Misc/Functions.php Normal file
View File

@@ -0,0 +1,47 @@
<?php
if (! function_exists('__j')) {
/**
* Translate the given message and prepare it for using in JS.
*/
function __j($key, $replace = [], $locale = null)
{
$text = __($key, $replace, $locale);
return strtr($text, [
'"' => '&quot;',
"'" => '&#039;',
]);
}
/**
* Translates and escapes the text for using in HTML.
*/
function __h($key, $replace = [], $locale = null, $escape_replacements = false)
{
if ($escape_replacements) {
foreach ($replace as $key => $value) {
$replace[$key] = htmlspecialchars($value);
}
}
return __(htmlspecialchars($key), $replace, $locale);
}
/**
* Strips unsafe tags.
*/
function safe_raw_html($text, $allowed_tags = [])
{
return \Helper::stripDangerousTags($text ?? '', $allowed_tags);
}
/**
* Translates and strips unsafe tags.
*/
function __safe_raw_html($key, $replace = [], $locale = null, $allowed_tags = [])
{
$text = __($key ?? '', $replace, $locale);
return \Helper::stripDangerousTags($text, $allowed_tags);
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Misc;
class SwiftGetSmtpQueueId implements \Swift_Events_ResponseListener
{
public static $last_smtp_queue_id = null;
public function responseReceived(\Swift_Events_ResponseEvent $evt)
{
$response_text = $evt->getResponse();
if (strpos($response_text, 'queued') !== false) {
preg_match("#queued as ([^\$\r\n ]+)[$\r\n]#", $response_text, $m);
if (!empty($m[1]) && trim($m[1])) {
self::$last_smtp_queue_id = trim($m[1]);
}
}
}
}

View File

@@ -37,20 +37,18 @@ class WpApi
$url .= '?';
}
$url .= 'v='.config('app.version');
return $client->request('POST', $url, [
'timeout' => config('app.curl_timeout'), // seconds
return $client->request('POST', $url, \Helper::setGuzzleDefaultOptions([
'connect_timeout' => 10,
'form_params' => $params,
'proxy' => config('app.proxy'),
]);
'allow_redirects' => ['strict' => true],
]));
} else {
$params['v'] = config('app.version');
return $client->request('GET', $url, [
'timeout' => config('app.curl_timeout'), // seconds
return $client->request('GET', $url, \Helper::setGuzzleDefaultOptions([
'connect_timeout' => 10,
'query' => $params,
'proxy' => config('app.proxy'),
]);
'allow_redirects' => ['strict' => true],
]));
}
}

View File

@@ -6,7 +6,9 @@
namespace App;
use App\Misc\WpApi;
use Illuminate\Database\Eloquent\Model;
use Symfony\Component\Console\Output\BufferedOutput;
class Module extends Model
{
@@ -33,6 +35,11 @@ class Module extends Model
return self::$modules;
}
public static function clearModulesCache()
{
self::$modules = null;
}
public static function isActive($alias)
{
$module = self::getByAlias($alias);
@@ -48,7 +55,21 @@ class Module extends Model
$module = self::getByAliasOrCreate($alias);
$module->active = $active;
if ($save) {
$module->save();
try {
$module->save();
} catch (\Exception $e) {
if (strstr($e->getMessage(), 'Integrity constraint violation')) {
// SQLSTATE[23000]: Integrity constraint violation.
self::clearModulesCache();
$module = self::getByAliasOrCreate($alias);
if ($module) {
$module->active = $active;
$module->save();
}
} else {
throw $e;
}
}
}
return true;
@@ -132,6 +153,30 @@ class Module extends Model
$module->save();
}
/**
* Automatically encrypt license key.
*/
public function setLicenseAttribute($value)
{
if ($value != '') {
$this->attributes['license'] = \Helper::encrypt($value);
} else {
$this->attributes['license'] = '';
}
}
/**
* Automatically decrypt license key.
*/
public function getLicenseAttribute($value)
{
if (!$value) {
return '';
}
return \Helper::decrypt($value) ?: $value;
}
public static function normalizeAlias($alias)
{
return trim(strtolower($alias));
@@ -237,10 +282,7 @@ class Module extends Model
public static function formatModuleData($module_data)
{
// Add (Third-Party).
if (\App\Module::isOfficial($module_data['authorUrl'])
&& $module_data['author'] != 'FreeScout'
&& mb_substr(trim($module_data['name']), -1) != ']'
) {
if (self::isThirdParty($module_data) && mb_substr(trim($module_data['name']), -1) != ']') {
$module_data['name'] = $module_data['name'].' ['.__('Third-Party').']';
}
return $module_data;
@@ -248,7 +290,7 @@ class Module extends Model
public static function isThirdParty($module_data)
{
if (\App\Module::isOfficial($module_data['authorUrl'])
if (\App\Module::isOfficial($module_data['detailsUrl'])
&& $module_data['author'] != 'FreeScout'
) {
return true;
@@ -256,4 +298,333 @@ class Module extends Model
return false;
}
}
public static function getSymlinkPath($alias)
{
return public_path().\Module::getPublicPath($alias);
}
// Check and try to fix invalid or missing symlinks.
public static function checkSymlinks($module_aliases = null)
{
$invalid_symlinks = [];
if ($module_aliases === null) {
// Get all active modules.
foreach (\Module::all() as $module) {
if ($module->active()) {
$module_aliases[] = $module->getAlias();
}
}
}
if ($module_aliases && count($module_aliases)) {
foreach ($module_aliases as $module_alias) {
$from = self::getSymlinkPath($module_alias);
$create = false;
// file_exists() also checks if symlink target exists.
// file_exists() and is_dir() may throw "open_basedir restriction in effect".
try {
if (!file_exists($from) || !is_link($from)) {
if (is_dir($from)) {
@rename($from, $from.'_'.date('YmdHis'));
} else {
@unlink($from);
}
$create = true;
}
} catch (\Exception $e) {
$create = true;
}
// Skip this check.
// elseif (is_link($from) && readlink($symlink_path) != '') {
// // Symlink leads to the wrong place.
// $create = true;
// }
// Try to create the symlink.
if ($create) {
$to = self::createModuleSymlink($module_alias);
if ($to && (!is_link($from) || is_link($to) || !file_exists($from))) {
$invalid_symlinks[$from] = $to;
}
}
}
}
return $invalid_symlinks;
}
// There is similar function in ModuleInstall.php
public static function createModuleSymlink($alias)
{
$from = self::getSymlinkPath($alias);
$module = \Module::findByAlias($alias);
if (!$module) {
return false;
}
$to = $module->getExtraPath('Public');
// file_exists() may throw "open_basedir restriction in effect".
try {
// If module's Public is symlink.
if (is_link($to)) {
@unlink($to);
}
// Symlimk may exist but lead to the module folder in a wrong case.
// So we need first try to remove it.
if (!file_exists($from)) {
@unlink($from);
}
if (file_exists($from)) {
return $to;
}
if (!file_exists($to)) {
// Try to create Public folder.
try {
\File::makeDirectory($to, \Helper::DIR_PERMISSIONS);
} catch (\Exception $e) {
// If it's a broken symlink.
if (is_link($to)) {
@unlink($to);
}
}
}
try {
symlink($to, $from);
} catch (\Exception $e) {
\Log::error('Error occurred creating ['.$from.' » '.$to.'] symlink: '.$e->getMessage());
//return false;
}
} catch (\Exception $e) {
return false;
}
return $to;
}
public static function updateModule($alias)
{
$result = [
'status' => 'error',
// Error message.
'msg' => '',
'msg_success' => '',
'download_error' => false,
// Error message with the link for downloading the module.
'download_msg' => '',
'output' => '',
'module_name' => '',
];
$module = \Module::findByAlias($alias);
if (!$module) {
$result['msg'] = __('Module not found').': '.$alias;
}
// Get module name.
$name = '?';
if ($module) {
$name = $module->getName();
$result['module_name'] = $name;
}
// Download new version.
if (!$result['msg']) {
// Check if the module's author is officially recognized
if (self::isOfficial($module->authorUrl)) {
$params = [
'license' => self::getLicense($alias),
'module_alias' => $alias,
'url' => self::getAppUrl(),
];
$license_details = WpApi::getVersion($params);
if (WpApi::$lastError) {
$result['msg'] = WpApi::$lastError['message'];
} elseif (!empty($license_details['code']) && !empty($license_details['message'])) {
$result['msg'] = $license_details['message'];
} elseif (!empty($license_details['required_app_version']) && !\Helper::checkAppVersion($license_details['required_app_version'])) {
$result['msg'] = 'Module requires app version:'.' '.$license_details['required_app_version'];
} elseif (!empty($license_details['download_link'])) {
// If a download link is available, proceed to update the module from the URL
$result = self::updateFromUrl($module, $license_details['download_link'], $result);
} elseif ($license_details['status'] && $result['msg'] = self::getErrorMessage($license_details['status'])) {
//$result['msg'] = ;
} else {
$result['msg'] = __('Error occurred').': '.json_encode($license_details);
}
} else {
// If the module's author is not officially recognized, check for a direct download link
$latest_version_zip_url = $module->latestVersionZipUrl ?? null;
if (!empty($latest_version_zip_url)) {
// Update the module from the provided ZIP URL
$result = self::updateFromUrl($module, $module->latestVersionZipUrl, $result);
} else {
// If no download link is available, set an error message indicating the module cannot be downloaded
$result['msg'] = __('Error occurred') . ': module not available for download';
}
}
}
// Run post-update instructions.
if (!$result['msg'] && !$result['download_error']) {
$output_log = new BufferedOutput();
\Artisan::call('freescout:module-install', ['module_alias' => $alias], $output_log);
$result['output'] = $output_log->fetch() ?: ' ';
$result['msg'] = __('Error occurred activating ":name" module', ['name' => $name]);
if (session('flashes_floating') && is_array(session('flashes_floating'))) {
// Error.
// If there was any error, module has been deactivated via modules.register_error filter
$result['msg'] = '';
foreach (session('flashes_floating') as $flash) {
$result['msg'] .= $flash['text'].' ';
}
} elseif (strstr($result['output'], 'Configuration cached successfully')) {
// Success.
$result['status'] = 'success';
$result['msg'] = '';
$result['msg_success'] = __('":name" module successfully updated!', ['name' => $name]);
} else {
// Error.
// Deactivate module.
\App\Module::setActive($alias, false);
\Artisan::call('freescout:clear-cache');
}
}
return $result;
}
/**
* Updates a module from a given URL.
*
* @param Module $module The module to be updated.
* @param string $url The URL where the new version of the module can be downloaded.
* @param array $result An associative array to store the result of the update operation.
*
* @return void
*/
private static function updateFromUrl($module, $url, $result)
{
$alias = $module->alias;
// Download module.
$module_archive = \Module::getPath() . DIRECTORY_SEPARATOR . $alias . '.zip';
try {
\Helper::downloadRemoteFile($url, $module_archive);
} catch (\Exception $e) {
$result['msg'] = $e->getMessage();
}
if (!file_exists($module_archive)) {
$result['download_error'] = true;
} else {
// Extract.
try {
$module_path = $module->getPath();
// Sometimes by some reason Public folder becomes a symlink leading to itself.
// It causes an error during updating process.
// https://github.com/freescout-helpdesk/freescout/issues/2709
$public_folder = $module_path.DIRECTORY_SEPARATOR.'Public';
try {
if (is_link($public_folder)) {
unlink($public_folder);
}
} catch (\Exception $e) {
// Do nothing.
}
\Helper::unzip($module_archive, \Module::getPath());
// Rename the folder if the archive has been downloaded from GitHub:
// SomeModule-master >> SomeModule.
// https://github.com/freescout-help-desk/freescout/issues/4611
$basename = basename($url, '.zip');
if (file_exists($module_path.'-'.$basename)) {
\File::moveDirectory($module_path.'-'.$basename, $module_path, true);
}
} catch (\Exception $e) {
$result['msg'] = $e->getMessage();
}
// Check if extracted module exists.
\Module::clearCache();
$module = \Module::findByAlias($alias);
if (!$module) {
$result['download_error'] = true;
}
}
// Remove archive.
if (file_exists($module_archive)) {
\File::delete($module_archive);
}
if ($result['download_error']) {
$result['download_msg'] = __('Error occurred downloading the module. Please :%a_being%download:%a_end% module manually and extract into :folder', [
'%a_being%' => '<a href="' . $url . '" target="_blank">',
'%a_end%' => '</a>',
'folder' => '<strong>' . \Module::getPath() . '</strong>',
]);
}
return $result;
}
public static function getErrorMessage($code, $result = null)
{
$msg = '';
switch ($code) {
case 'missing':
$msg = __('License key does not exist');
break;
case 'license_not_activable':
$msg = __("You have to activate each bundle's module separately");
break;
case 'disabled':
$msg = __('License key has been revoked');
break;
case 'no_activations_left':
$msg = __('No activations left for this license key').' ('.__("Use 'Deactivate License' link above to transfer license key from another domain").')';
break;
case 'expired':
$msg = __('License key has expired');
break;
case 'key_mismatch':
$msg = __('License key belongs to another module');
break;
// This also happens when entering a valid license key for wrong module.
case 'invalid_item_id':
$msg = __('Invalid license key');
//$msg = __('Module not found in the modules directory');
break;
case 'site_inactive':
$msg = __('License key is activated on another domain.').' '.__("Use 'Deactivate License' link above to transfer license key from another domain");
//$msg = __('Module not found in the modules directory');
break;
default:
if ($result && !empty($result['error'])) {
$msg = __('Error code:'.' '.$result['error']);
}
break;
}
return $msg;
}
}

View File

@@ -68,16 +68,32 @@ class BroadcastNotification extends Notification implements ShouldQueue
{
$data = [];
$user = auth()->user();
if (empty($payload->thread_id) || empty($payload->mediums)) {
return $data;
}
// Try to convert to array.
$mediums = (array)$payload->mediums;
$thread = Thread::find($payload->thread_id);
$mailbox = ($thread && $thread->conversation) ? $thread->conversation->mailbox : null;
if (empty($thread)) {
if (empty($thread) || !$user || !$mailbox || !$user->can('viewCached', $mailbox)) {
return $data;
}
$conversation = $thread->conversation;
// Play audio notification if this is Chat conversation.
//$now = time();
if ($conversation->isChat() /*&& $conversation->last_reply_at && $conversation->last_reply_at->timestamp < ($now - 10*60)*/) {
$data['audio'] = [
'thread_id' => $thread->id,
];
}
if (empty($payload->mediums)) {
return $data;
}
@@ -90,7 +106,7 @@ class BroadcastNotification extends Notification implements ShouldQueue
// Get last reply or note of the conversation to display it's text
$last_thread_body = '';
$last_thread = Thread::where('conversation_id', $thread->conversation_id)
$last_thread = Thread::where('conversation_id', $conversation->id)
// Select must contain all fields from orderBy() to avoid:
// General error: 3065 Expression #1 of ORDER BY clause is not in SELECT
->select(['body', 'created_at'])
@@ -107,7 +123,7 @@ class BroadcastNotification extends Notification implements ShouldQueue
$web_notifications_info['created_at'] = \Carbon\Carbon::now();
// ['notification']->read_at
// ['notification']->id
$web_notifications_info['conversation'] = $thread->conversation;
$web_notifications_info['conversation'] = $conversation;
$web_notifications_info['thread'] = $thread;
$web_notifications_info['last_thread_body'] = $last_thread_body;
@@ -118,8 +134,8 @@ class BroadcastNotification extends Notification implements ShouldQueue
// Text and url for the browser push notification
if (in_array(Subscription::MEDIUM_BROWSER, $mediums)) {
$data['browser']['text'] = strip_tags($thread->getActionDescription($thread->conversation->number));
$data['browser']['url'] = $thread->conversation->url(null, $thread->id, ['mark_as_read' => $db_notification->id]);
$data['browser']['text'] = \Helper::stripTags($thread->getActionDescription($conversation->number));
$data['browser']['url'] = $conversation->url(null, $thread->id, ['mark_as_read' => $db_notification->id]);
}
return $data;

View File

@@ -19,15 +19,18 @@ class WebsiteNotification extends Notification implements ShouldQueue
public $thread;
public $data;
/**
* Create a new notification instance.
*
* @return void
*/
public function __construct($conversation, $thread)
public function __construct($conversation, $thread, $data = [])
{
$this->conversation = $conversation;
$this->thread = $thread;
$this->data = $data;
}
/**
@@ -54,13 +57,14 @@ class WebsiteNotification extends Notification implements ShouldQueue
return [
'thread_id' => $this->thread->id,
'conversation_id' => $this->conversation->id,
'data' => $this->data,
];
}
/**
* Fetch data from DB for notifications list to display it.
*/
public static function fetchNotificationsData($notifications)
public static function fetchNotificationsData($notifications, $auth_user = null)
{
$data = [];
@@ -141,6 +145,13 @@ class WebsiteNotification extends Notification implements ShouldQueue
continue;
}
// Check access to the conversation
// and reset $last_thread_body if there is no access anymore.
// https://github.com/freescout-help-desk/freescout/security/advisories/GHSA-6545-8c3f-pp6w
if (!$auth_user->can('view', $conversation)) {
$last_thread_body = '…';
}
$data[] = [
'notification' => $notification,
'created_at' => $notification->created_at,
@@ -150,6 +161,7 @@ class WebsiteNotification extends Notification implements ShouldQueue
'user' => $user,
'created_by_user' => $created_by_user,
'created_by_customer' => $created_by_customer,
'data' => $notification->data['data'] ?? [],
];
}

View File

@@ -16,6 +16,8 @@ class ConversationObserver
if ($conversation->source_via == Conversation::PERSON_USER) {
$conversation->read_by_user = true;
}
$conversation->subject = mb_substr($conversation->subject ?? '', 0, Conversation::SUBJECT_MAXLENGTH);
}
/**
@@ -29,6 +31,17 @@ class ConversationObserver
//$conversation->mailbox->updateFoldersCounters();
}
/**
* On before updating.
*
* @param Conversation $conversation
*/
public function updating(Conversation $conversation)
{
// https://github.com/freescout-help-desk/freescout/issues/5201
$conversation->subject = mb_substr($conversation->subject ?? '', 0, Conversation::SUBJECT_MAXLENGTH);
}
/**
* On conversation delete.
*

View File

@@ -3,6 +3,7 @@
namespace App\Observers;
use App\Customer;
use App\CustomerChannel;
class CustomerObserver
{
@@ -17,4 +18,16 @@ class CustomerObserver
{
\Eventy::action('customer.deleting', $customer);
}
public function created(Customer $customer)
{
if ($customer->channel && $customer->channel_id) {
CustomerChannel::create($customer->id, $customer->channel, $customer->channel_id);
}
}
public function deleted(Customer $customer)
{
CustomerChannel::where('customer_id', $customer->id)->delete();
}
}

View File

@@ -6,6 +6,17 @@ use Illuminate\Notifications\DatabaseNotification;
class DatabaseNotificationObserver
{
/**
* Populate "conversation_id" column.
*/
public function creating(DatabaseNotification $notification)
{
// $notification->data is already cast to array by the base model.
if (!empty($notification->data['conversation_id'])) {
$notification->conversation_id = $notification->data['conversation_id'];
}
}
/**
* Notifications DB record created.
*

View File

@@ -6,6 +6,17 @@ use App\Email;
class EmailObserver
{
/**
* On create before saving.
*
* @param Email $email
*/
public function creating(Email $email)
{
// https://github.com/freescout-help-desk/freescout/issues/5106
$email->email = Email::sanitizeLength($email->email);
}
/**
* Email created.
*

View File

@@ -8,7 +8,7 @@ class FollowerObserver
{
public function created(Follower $follower)
{
\Eventy::action('follower.created', $attachment);
\Eventy::action('follower.created', $follower);
}
public function deleted(Follower $follower)

View File

@@ -33,10 +33,16 @@ class MailboxObserver
*/
public function deleting(Mailbox $mailbox)
{
// Same things are deleted in Mailbox->deleteMailbox().
$mailbox->users()->delete();
$mailbox->conversations()->delete();
$mailbox->folders()->delete();
\Eventy::action('mailbox.before_delete', $mailbox);
}
public function deleted(Mailbox $mailbox)
{
\Eventy::action('mailbox.deleted', $mailbox);
}
}

View File

@@ -21,7 +21,15 @@ class ThreadObserver
return;
}
$now = date('Y-m-d H:i:s');
// Fetch date & time setting.
$use_mail_date_on_fetching = config('app.use_mail_date_on_fetching');
if ($use_mail_date_on_fetching) {
$now = $thread->created_at->format('Y-m-d H:i:s');
} else {
$now = date('Y-m-d H:i:s');
}
if (!in_array($thread->type, [Thread::TYPE_LINEITEM, Thread::TYPE_NOTE]) && $thread->state == Thread::STATE_PUBLISHED) {
$conversation->threads_count++;
}
@@ -30,12 +38,18 @@ class ThreadObserver
}
if ((in_array($thread->type, [Thread::TYPE_CUSTOMER, Thread::TYPE_MESSAGE])
|| ($conversation->isPhone() && in_array($thread->type, [Thread::TYPE_NOTE])))
|| ($conversation->isPhone() && in_array($thread->type, [Thread::TYPE_NOTE]))
// https://github.com/freescout-help-desk/freescout/issues/5105
// This is not a normal situtaion: email conversation containing just one Note thread.
|| ($conversation->threads_count == 0 && $thread->type == Thread::TYPE_NOTE)
)
&& $thread->state == Thread::STATE_PUBLISHED
) {
// $conversation->cc = $thread->cc;
// $conversation->bcc = $thread->bcc;
$conversation->last_reply_at = $now;
$conversation->setLastReplyAt($now, $thread->source_via);
// "last_reply_from" may be already set at this stage (by FetchEmails for example).
$conversation->last_reply_from = $thread->source_via;
}
if ($conversation->source_via == Conversation::PERSON_CUSTOMER) {

View File

@@ -1,6 +1,6 @@
<?php
/**
* todo: implement caching by saving all options in one cache variable on register_shutdown_function.
* Todo: implement caching by saving all options in one cache variable on register_shutdown_function.
*/
namespace App;
@@ -51,7 +51,8 @@ class Option extends Model
$serialized_value = self::maybeSerialize($value);
$option = self::firstOrCreate(
['name' => $name], ['value' => $serialized_value]
['name' => $name],
['value' => $serialized_value]
);
$old_value = $option['value'];
@@ -205,8 +206,16 @@ class Option extends Model
*/
public static function maybeSerialize($data)
{
if (is_array($data) || is_object($data)) {
return serialize($data);
// if (is_array($data) || is_object($data)) {
// return serialize($data);
// }
// We don't use serialize() function as it is not safe.
if (is_array($data)) {
try {
return \Helper::jsonEncodeSafe($data);
} catch (\Exception $e) {
// Don nothing.
}
}
return $data;
@@ -217,9 +226,11 @@ class Option extends Model
*/
public static function maybeUnserialize($original)
{
// This is kept jsut for backward compatibility.
// Options are not seralized using serialize() anymore.
if (self::isSerialized($original)) {
try {
$original = unserialize($original);
$original = unserialize($original, ['allowed_classes' => false]);
} catch (\Exception $e) {
// Do nothing
}
@@ -227,6 +238,19 @@ class Option extends Model
return $original;
}
if (is_string($original) && !empty($original[0])
&& ($original[0] == '{' || $original[0] == '[')
) {
try {
$result = json_decode($original, true);
if (is_array($result)) {
return $result;
}
} catch (\Exception $e) {
// Do nothing.
}
}
return $original;
}

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