Compare commits

...

1832 Commits

Author SHA1 Message Date
FreeScout
8053591031 Add vendor files to dist 2022-03-29 06:13:20 -07:00
FreeScout
3d2fcd020d Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2022-03-29 06:11:56 -07:00
FreeScout
3d7fd68fa8 v1.8.10 2022-03-29 06:11:25 -07:00
FreeScout
1a011d9ea4 Fix Object of class Webklex\PHPIMAP\Header could not be converted to string - closes #1852 2022-03-29 01:18:22 -07:00
FreeScout
57a943edec Fix invalid encoding "default" on PHP 8 - closes #1837 2022-03-29 00:46:37 -07:00
FreeScout
85e9dc53f2 Ensure that there is always a Customer email address when updating a conversation #1847 from edwardhughes/master
Ensure that there is always a Customer email address when updating a conversation
2022-03-29 10:15:16 +03:00
FreeScout
a682d8f7df Add empty string check to hasManageMailboxAccess() #1850 from Jobs-Foundation/master
Add empty string check to hasManageMailboxAccess()
2022-03-29 10:12:47 +03:00
FreeScout
bdc1073a28 Limit user dropdown height in bulk actions - closes #1840 2022-03-29 00:11:32 -07:00
Zachary Schroeder
ac79c57de4 add empty string check 2022-03-28 11:08:36 -05:00
Edward Hughes IV
1b70e45a1c Ensure that there is always a Customer email address when updating a Conversation. 2022-03-25 14:37:54 -04:00
FreeScout
97d0c470fb Fix null to parameter in Swiftmailer - closes #1831 2022-03-20 07:01:08 -07:00
FreeScout
c92589da2a Skip error in setUrl() - closes #1789 2022-03-19 23:32:08 -07:00
FreeScout
3dffd231ae Update FR translation 2022-03-19 23:19:33 -07:00
FreeScout
f429bdef8e Allow PHP 8 in update.sh - closes #1819 2022-03-19 23:14:34 -07:00
FreeScout
11f223ab04 Persian language 2022-03-14 04:18:46 -07:00
FreeScout
2ccf407bd9 RTL adjustments 2022-03-14 00:21:41 -07:00
FreeScout
a172d476f5 Merge pull request #1750 from rhashnull/master
Add RTL and Fa-IR language support for project.
2022-03-14 10:17:22 +03:00
FreeScout
f37303e8a0 Fix recipients names when sending new conversations separately - closes #1802 2022-03-14 00:06:56 -07:00
FreeScout
60016fd532 Show instructions on error 500 page - closes #1808 2022-03-13 06:26:11 -07:00
FreeScout
ab3de1f60b Show Microsoft OAuth connect link only if username is not an email 2022-03-13 01:14:49 -08:00
FreeScout
6014509dd2 Add vendor files to dist 2022-03-13 01:03:57 -08:00
FreeScout
4e856ea783 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2022-03-13 01:01:34 -08:00
FreeScout
23aeefbcbd v1.8.9 2022-03-13 01:01:03 -08:00
FreeScout
b84afb99a5 IMAP oAuth 2022-03-13 00:55:59 -08:00
FreeScout
2dada15292 Merge branch 'oauth2' 2022-03-13 00:54:03 -08:00
FreeScout
a87e2199f0 IMAP oAuth 2022-03-13 00:53:01 -08:00
Arash Khajelou
0e85c73a77 Fix the key name for locales_rtl config. 2022-03-12 15:19:06 +03:30
Arash Khajelou
d7ef54a37e Reverse the format for app.blade.php file. 2022-03-12 15:15:50 +03:30
Arash Khajelou
ed47a5e3c1 Merge branch 'freescout-22-03-12-master' 2022-03-12 15:13:03 +03:30
Arash Khajelou
27698ebea0 Cleanup and add the needed style for rtl locales. 2022-03-12 15:09:40 +03:30
Arash Khajelou
3bcdcd9284 Add locale-{{ app()->getLocale() }} to the body and load Yekan font only for .locale-fa. 2022-03-12 15:09:40 +03:30
Arash Khajelou
640379e4d7 If some language is RTL add dir="rtl" attribute to <html> tag and "rtl" class to the <body> 2022-03-12 15:09:40 +03:30
Arash Khajelou
47b4dfb4b8 Reverse the format of file Helper.php 2022-03-12 15:09:37 +03:30
Arash Khajelou
da2ea1cd1b Add to /app/Misc/Helper.php the isLocaleRtl() method to check if current language is RTL using the config parameter. 2022-03-12 15:08:24 +03:30
Arash Khajelou
12e80d60b6 Add to app.php a list of RTL languages: 'locales_rtl' => ['fa'], 2022-03-12 15:04:14 +03:30
Arash Khajelou
141fd8320e Remove .envrc file 2022-03-12 15:04:14 +03:30
Arash Khajelou
a9ac930cc0 Add persian language support to the project. 2022-03-12 15:04:14 +03:30
Arash Khajelou
344f06178f Cleanup and add the needed style for rtl locales. 2022-03-12 14:55:31 +03:30
Arash Khajelou
0f03f4fffd Add locale-{{ app()->getLocale() }} to the body and load Yekan font only for .locale-fa. 2022-03-12 14:53:44 +03:30
Arash Khajelou
24ef816c2e If some language is RTL add dir="rtl" attribute to <html> tag and "rtl" class to the <body> 2022-03-12 14:51:22 +03:30
Arash Khajelou
516e02be1d Reverse the format of file Helper.php 2022-03-12 14:47:19 +03:30
Arash Khajelou
3c13551a7a Add to /app/Misc/Helper.php the isLocaleRtl() method to check if current language is RTL using the config parameter. 2022-03-12 14:43:50 +03:30
Arash Khajelou
efcf05ad8b Add to app.php a list of RTL languages: 'locales_rtl' => ['fa'], 2022-03-12 14:37:08 +03:30
Arash Khajelou
26b7e1c5c3 Remove .envrc file 2022-03-12 14:31:10 +03:30
FreeScout
2e71180f7c Fix saving System Emails page with empty password on PHP 8.1 2022-03-10 04:02:12 -08:00
FreeScout
a42e0db899 IMAP oAuth - embedded images 2022-03-10 01:26:24 -08:00
FreeScout
3c7dda7118 IMAP oAuth - help link 2022-03-09 06:13:22 -08:00
FreeScout
740d8aa706 IMAP oAuth 2022-03-09 05:24:22 -08:00
FreeScout
e82a1c2b81 IMAP oAuth - refresh access token 2022-03-09 03:45:15 -08:00
FreeScout
7ea7795913 Fix home ulr on 404 - closes #1762 2022-03-09 02:06:24 -08:00
FreeScout
2589aa1da9 IMAP oAuth 2022-03-09 01:10:16 -08:00
FreeScout
18295e83fb Issue template 2022-03-08 22:14:22 -08:00
FreeScout
768c4561a9 IMAP oAuth 2022-03-08 22:13:25 -08:00
FreeScout
dcb7e7715f Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2022-03-04 09:22:48 -08:00
FreeScout
767b20f059 v1.8.8 2022-03-04 09:22:23 -08:00
FreeScout
66fba5f20a Take Spread the Word option into accoutn in test mailbox email - #1773 2022-03-04 04:09:13 -08:00
FreeScout
921186e2e0 Fix issue with searching closes #1788 2022-03-04 04:01:15 -08:00
FreeScout
5d41b6908c Issue template 2022-03-04 03:26:47 -08:00
FreeScout
fede8d633a Issue template 2022-03-04 01:19:11 -08:00
FreeScout
182f49f758 IMAP oAuth 2022-03-04 01:13:29 -08:00
FreeScout
91c914fd14 Allow to save fetching settings with empty password on PHP 8.1 2022-02-28 00:57:25 -08:00
FreeScout
c6ea0a85cb Fix PHP 8.1 error when opening fetching settings for a new mailbox 2022-02-28 00:55:18 -08:00
FreeScout
0a6e0a100c Helper currentUrl() 2022-02-26 05:49:39 -08:00
FreeScout
d4b81c4cdb Merge pull request #1768 from Wouter0100/fix/remove-mailbox-cc
Do not add mailbox emails itself to CC on fetching
2022-02-26 08:18:06 +03:00
FreeScout
2b4f67befe Change homepage link on 404 page - closes #1762 2022-02-25 21:11:55 -08:00
Wouter van Os
56597bbd29 fix: do not add mailbox emails itself to CC in new conversation
This fixes a loop that is caused by moving a conversation to
another mailbox. In that case the old mailbox was still visible
as a CC when replying on the moved converstaion. This CC would
result in a new converstation being created.
2022-02-25 17:42:17 +01:00
FreeScout
c0fc874ce4 If custom login page path set do not show login form at /login - closes #1751 2022-02-25 06:25:19 -08:00
FreeScout
d26ab69b18 Fix underscores in encoded subjects 2022-02-22 22:21:01 -08:00
FreeScout
346148ae60 Fix underscores in encoded subjects - closes #1744 2022-02-22 22:10:42 -08:00
FreeScout
8e9bcc6040 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2022-02-22 08:49:20 -08:00
FreeScout
f4c76467bb v1.8.7 2022-02-22 08:48:57 -08:00
FreeScout
26a8d8511f Merge branch 'master' of github.com:freescout-helpdesk/freescout 2022-02-22 02:34:21 -08:00
FreeScout
1873dc6a20 Update modules check 2022-02-22 00:07:20 -08:00
FreeScout
8fa94d8452 Merge pull request #1748 from Lars-/master
Added hooks in preparation for custom signatures module
2022-02-21 09:21:50 +03:00
FreeScout
d62088fa1f Merge pull request #1754 from psuet/patch-1
Add autocomplete Tags to Auth Views
2022-02-21 09:19:27 +03:00
Lars Jansen
03d8e2e0e8 Update update.blade.php 2022-02-20 09:32:44 +01:00
Lars Jansen
4397e0c756 Update editor_bottom_toolbar.blade.php 2022-02-20 09:32:16 +01:00
Lars Jansen
42b23f6e3a Update main.js 2022-02-20 09:31:38 +01:00
Lars Jansen
501b0719cc Update MailboxesController.php 2022-02-20 09:30:39 +01:00
Lars Jansen
1a7289a395 Update ConversationsController.php 2022-02-20 09:30:09 +01:00
Lars Jansen
6153db71c8 Update Conversation.php 2022-02-20 09:29:06 +01:00
Paul Sütterlin
5a7eae76d7 Add autocomplete Tags to Auth Views 2022-02-20 06:21:16 +01:00
Arash Khajelou
cc8e5da17b Add persian language support to the project. 2022-02-19 17:04:24 +03:30
Lars Jansen
54d44ba87a Update editor_bottom_toolbar.blade.php 2022-02-18 15:58:08 +01:00
Lars Jansen
64c46e3941 Update update.blade.php 2022-02-18 15:44:02 +01:00
Lars Jansen
2458539c6f Added hooks in preparation for custom signatures module 2022-02-18 15:31:38 +01:00
FreeScout
c56b9a0573 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2022-02-17 21:51:11 -08:00
FreeScout
eeb00763d3 v1.8.6 2022-02-17 21:50:46 -08:00
FreeScout
800a484acc mail_vars.replace hook 2022-02-16 22:40:25 -08:00
FreeScout
b3e444be47 Merge pull request #1742 from Lars-/master
Mail vars hook
2022-02-17 09:37:41 +03:00
FreeScout
ff025b94d2 Adjust command cron schedule 2022-02-16 22:35:09 -08:00
Lars Jansen
96986e8a22 Allow custom mail vars 2022-02-17 07:25:17 +01:00
FreeScout
2d0c72d5bb PULL_REQUEST_TEMPLATE 2022-02-16 22:11:07 -08:00
FreeScout
384ff40d88 Fix parsing dates with underscores - closes #1738 2022-02-16 04:04:52 -08:00
FreeScout
8dca28adeb Fix preg_match issue in Str.php 2022-02-16 04:04:07 -08:00
FreeScout
1f6a95c4a7 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2022-02-15 05:35:30 -08:00
FreeScout
3493f16035 v1.8.5 2022-02-15 05:34:40 -08:00
FreeScout
97d12be54d Fix command creating modules 2022-02-14 22:11:11 -08:00
FreeScout
19d84bb479 Improve Module::isActive() method performance 2022-02-14 01:11:00 -08:00
FreeScout
2a29e3194e Fix empty parameter passing to strstr() in connection.blade.php 2022-02-13 23:48:54 -08:00
FreeScout
4b8904f335 parseDateToCarbon() helper function 2022-02-13 23:48:35 -08:00
FreeScout
a9d43435e9 Add aria-labels to select2 options 2022-02-13 23:26:12 -08:00
FreeScout
9884a767ff Add aria-labels to summernote buttons 2022-02-13 22:59:48 -08:00
FreeScout
aa1dba6241 Clean summernote folder 2022-02-13 22:38:53 -08:00
FreeScout
ba9293a598 Adjust conversation.can_submit hook 2022-02-08 22:02:47 -08:00
FreeScout
7a15e55da2 Fix null issue in Purifier 2022-02-08 06:34:12 -08:00
FreeScout
77a851cbe0 Do not allow users and mailboxes have the same email addresses 2022-02-08 03:46:23 -08:00
FreeScout
595eaa7a20 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2022-02-07 02:30:35 -08:00
FreeScout
a66b6d019e v1.8.4 2022-02-07 02:30:03 -08:00
FreeScout
b6257aa21c Merge branch 'master' of github.com:freescout-helpdesk/freescout 2022-02-07 02:29:08 -08:00
FreeScout
1a46c7f001 Failed opening required compat/vlucas/phpdotenv/src/Loader.php - closes #1714 2022-02-07 02:29:01 -08:00
FreeScout
8ddbe18543 Merge pull request #1715 from afbora/fix/1713-php8-compatibility
Fix Translation Manager PHP 8 compatibility
2022-02-07 13:11:50 +03:00
Ahmet Bora
5c8de33eb6 Fix php 8 compatibility #1713 2022-02-07 12:03:32 +03:00
FreeScout
7c663cacf8 Add vendor files to dist 2022-02-04 23:53:37 -08:00
FreeScout
1bedf8394d Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2022-02-04 23:49:49 -08:00
FreeScout
df0ca8b740 v1.8.3 2022-02-04 23:49:00 -08:00
FreeScout
ced188f286 Flip reply icon - closes #1699 2022-02-04 22:37:31 -08:00
FreeScout
96fa6b5507 Store all vendor overrides in overrides folder 2022-02-04 07:18:23 -08:00
FreeScout
c19600bc7c Store all vendor overrides in overrides folder 2022-02-04 04:18:23 -08:00
FreeScout
cbddefcda2 Add vendor files to dist 2022-02-04 00:26:30 -08:00
FreeScout
a06d2f4c81 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2022-02-04 00:24:16 -08:00
FreeScout
929fc80e66 v1.8.2 2022-02-04 00:24:02 -08:00
FreeScout
56f2f1053b Preserve in vendor exclude-from-classmap files 2022-02-04 00:22:18 -08:00
FreeScout
3f1ec34dea Add vendor files to dist 2022-02-03 23:40:05 -08:00
FreeScout
ddc4f9d452 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2022-02-03 23:36:54 -08:00
FreeScout
14ae10536f v1.8.1 2022-02-03 23:36:16 -08:00
FreeScout
a9a3863d05 Fix ?: operator 2022-02-03 23:30:48 -08:00
FreeScout
9e8ebe17ea PHP 8.1 compatibility 2022-02-03 09:43:12 -08:00
FreeScout
9cca472d29 Add vendor files to dist 2022-02-03 04:03:34 -08:00
FreeScout
44390a6280 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2022-02-03 03:57:32 -08:00
FreeScout
345cdb2702 v1.8.0 2022-02-03 03:57:01 -08:00
FreeScout
a2adf76962 PHP 8.1 compatibility 2022-02-03 03:47:48 -08:00
FreeScout
3d477f6296 Add vendor files to dist 2022-02-02 22:49:30 -08:00
FreeScout
b0469ed5bc Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2022-02-02 22:48:17 -08:00
FreeScout
63f5b92a3c PHP 8.1 compatibility 2022-02-02 22:48:10 -08:00
FreeScout
a5d03df940 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2022-02-02 22:26:06 -08:00
FreeScout
2b07bcbca7 PHP 8.1 compatibility - closes #1691 2022-02-02 22:25:12 -08:00
FreeScout
d55bfd6495 Optimize search query 2022-01-31 21:35:39 -08:00
FreeScout
8210891675 Fix redirecting to root in subdirectory 2022-01-31 21:35:18 -08:00
FreeScout
beeeadf34a Merge pull request #1689 from GaneshKandu/patch-2
Fix redirecting to root in subdirectory
2022-02-01 08:30:35 +03:00
Ganesh Kandu
1f8b2098cc Freescout 1.7.30 redirecting to root
#1668 
This can be solution of this issue

if ```APP_DASHBOARD_PATH``` is empty it will redirect to ```/``` only
2022-01-31 14:26:38 +05:30
FreeScout
669ebf79e1 Optimize search query 2022-01-31 00:29:24 -08:00
FreeScout
8a0077c94d Fix search navigation with complex search filters 2022-01-31 00:08:43 -08:00
FreeScout
a2910deed3 Fix customer conversations pagination 2022-01-30 23:37:47 -08:00
FreeScout
039623097d Conversation::search() function 2022-01-30 05:43:18 -08:00
FreeScout
833158b0de Light color for spam conversations - closes #1680 2022-01-29 06:13:58 -08:00
FreeScout
d1598d61ac Add Collision detection to Readme 2022-01-29 05:57:28 -08:00
FreeScout
672149d3f7 Check if mailbox exists in SendReplyToCustomer job - closes #1678 2022-01-29 05:53:59 -08:00
FreeScout
5db4c5ced2 Show merged conversation link in the notice - closes #1686 2022-01-29 05:50:32 -08:00
FreeScout
8f07621946 Remove MS SQL from installation wizard - closes #1685 2022-01-29 02:04:32 -08:00
FreeScout
3235e60f72 Set all required parameters in Conversation::create() to avoid PostgreSQL error 2022-01-25 22:23:02 -08:00
FreeScout
566640d287 Set default options in \Helper::htmlToText() no to split the text 2022-01-24 22:58:33 -08:00
FreeScout
c10686e546 Fix customers search on PostgreSQL - closes #1664 2022-01-24 09:37:50 -08:00
FreeScout
fb1ac61d64 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2022-01-24 01:14:00 -08:00
FreeScout
813bc84575 v1.7.30 2022-01-24 01:12:58 -08:00
FreeScout
323e9d7a20 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2022-01-23 22:44:39 -08:00
FreeScout
f134a0288e v1.7.29 2022-01-23 22:43:21 -08:00
FreeScout
7324f15c50 Fix sorting conversations - closes #1662 2022-01-23 22:39:38 -08:00
FreeScout
b8286e7875 Clear note from cache after sending it - closes #1434 2022-01-22 22:09:05 -08:00
FreeScout
1afb66ce0b Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2022-01-22 01:24:55 -08:00
FreeScout
ec4670757e v1.7.28 2022-01-22 01:24:20 -08:00
FreeScout
92a9e7cb78 Helper isPgSql() function 2022-01-22 01:21:51 -08:00
FreeScout
dd8e8274a1 Improve sorting convesations table 2022-01-21 23:53:12 -08:00
FreeScout
b010996391 Merge pull request #1651 from alinmbb/master
Sorting conversations by columns
2022-01-22 09:07:46 +03:00
FreeScout
5bbe2b9d73 search.conversations_tab_append hook 2022-01-21 08:24:03 -08:00
FreeScout
c2336d9ca8 Fix Call to a member function isDraft() on null in SendNotificationToUsers - closes #1649 2022-01-21 06:48:01 -08:00
FreeScout
1b29571ba9 Add Integromat to Readme 2022-01-20 07:31:58 -08:00
mbb
810d2e3924 Added Conversations sorting by subject, Number, Creation Date in Mailbox view page 2022-01-20 09:23:44 +00:00
FreeScout
55d24994df search.conversations_tab_append hook 2022-01-15 06:31:48 -08:00
FreeScout
f0bba4b77b .fs-trigger-modal class to trigger modals 2022-01-15 06:31:28 -08:00
FreeScout
eefc7fd153 Allow to customize login path 2022-01-14 06:27:45 -08:00
FreeScout
f24b2e5040 Merge pull request #1628 from searsaw/pass-htmltotext-config
Add ability to pass config to Html2Text
2022-01-08 07:55:02 +03:00
Alex Sears
d96c180631 Add ability to pass config to Html2Text 2022-01-07 15:53:09 -05:00
FreeScout
f78933e2a4 Merge pull request #1623 from TylerB260/master
convsersation -> conversation Typo Fix
2022-01-06 08:20:36 +03:00
Tyler R. Bundy
b30b996e9c convsersation -> conversation 2022-01-05 13:14:21 -06:00
FreeScout
a51cb49225 Redirect to /home after login 2022-01-05 04:54:53 -08:00
FreeScout
f4924c3140 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2022-01-05 02:36:04 -08:00
FreeScout
c1b08383ef v1.7.27 2022-01-05 02:35:37 -08:00
FreeScout
59651ee24d guest_mode and no_footer options in the layout template 2022-01-05 00:21:05 -08:00
FreeScout
11b79b2b48 Add dashboard_path and home_controller to config 2022-01-04 23:52:53 -08:00
FreeScout
ac03db6380 menu.mailboxes hook 2022-01-04 01:31:36 -08:00
FreeScout
5d590d598e Always cache mailboxes in menu 2022-01-04 01:17:43 -08:00
FreeScout
17a157eb60 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2021-12-31 04:16:16 -08:00
FreeScout
2446a94d50 v1.7.26 2021-12-31 04:15:47 -08:00
FreeScout
53240b6360 deleteAttachments() function 2021-12-31 04:10:14 -08:00
FreeScout
b4b09213f7 Allow to pass user or user_id to Mailbox userHasAccess() 2021-12-31 04:00:36 -08:00
FreeScout
cdf6fd07cf thread.attachment_append hook 2021-12-31 01:08:12 -08:00
FreeScout
a0c5748c59 Merge pull request #1593 from Wouter0100/feature/add-fetching-email-hooks
Added hooks for custom fetching
2021-12-29 11:03:20 +03:00
Wouter van Os
d599c63816 Implemented @action in connection_incoming instead of foreach 2021-12-28 19:57:23 +01:00
FreeScout
c4fb834cd0 Allow to pass plain list of emails to Customer setData() 2021-12-28 06:59:24 -08:00
FreeScout
f605cf7e85 Allow to pass plain list of emails to Customer setData() 2021-12-28 06:38:38 -08:00
FreeScout
fa06b7148e Expand customer address block if non-empty 2021-12-28 05:51:13 -08:00
FreeScout
f17755aad8 Sanitize country in cusmer setData() 2021-12-28 05:49:05 -08:00
Wouter van Os
e4fdab0a19 Implemented feedback for implementing custom fetch providers
- Defaulting settings to visible
- Only add WHERE clause when not in_protocols is not empty
- Add tested arg to fetch_test
2021-12-27 18:01:45 +01:00
FreeScout
94af31c686 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2021-12-25 22:02:18 -08:00
FreeScout
28902b4c9f v1.7.25 2021-12-25 22:01:32 -08:00
FreeScout
e57ed73f98 Send attachments to all emails when sending to multiple recepients - closes #1599 2021-12-25 06:26:34 -08:00
FreeScout
bf1eb5985a Remove limitation on user Alternate Emails max length - closes #1592 2021-12-25 06:08:54 -08:00
FreeScout
2188ce8a4e Helper::setLocale() function 2021-12-24 23:41:13 -08:00
Wouter van Os
33767a590a Added hooks for custom fetch email providers 2021-12-23 23:19:19 +01:00
FreeScout
352469b662 Fix logs names in Logs 2021-12-15 22:42:54 -08:00
FreeScout
01b8da7cc2 conversations_table.before_mailbox hook 2021-12-13 09:26:42 -08:00
FreeScout
f9b517c849 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2021-12-13 06:41:47 -08:00
FreeScout
962418a9ee v1.7.24 2021-12-13 06:41:28 -08:00
FreeScout
3353d57c84 Merge pull request #1578 from philippkraft/master
Improve accessibility of modal dialogs (#1150)
2021-12-12 13:13:38 +03:00
FreeScout
0ed7db3b0c Fix last reply date for phone conversations 2021-12-09 22:44:26 -08:00
FreeScout
5ad58e5b25 conversations_table.before_mailbox hook 2021-12-09 01:17:12 -08:00
FreeScout
e28f174f20 Adjust default subscription implementation 2021-12-08 22:28:03 -08:00
FreeScout
4a8aae7aff Implement configuration for default subscriptions (fixes #294) 2021-12-09 08:28:59 +03:00
Philipp Kraft
e780a863cf Improve accessibility of modal dialogs (#1150)
Change aria-hidden attribute to aria-modal attribute to make modal dialogs accessible for screen readers
2021-12-08 20:35:55 +01:00
Moritz Marquardt
86c2724ceb Fix formatting (tabs vs. spaces) 2021-12-08 15:16:22 +01:00
Moritz Marquardt
6677cf39a9 Fix accessibility & translation issues and add German translation for default subscriptions 2021-12-08 15:01:44 +01:00
Moritz Marquardt
0bac1ae3c4 Make default subscription settings persistent 2021-12-08 14:51:02 +01:00
Moritz Marquardt
a152d8bf06 Add UI for subscriptions defaults 2021-12-08 14:39:04 +01:00
FreeScout
badba682a0 .dropdown-menu.with-icons style 2021-12-08 01:09:13 -08:00
FreeScout
90ae9e2a7e Conversations::create() should create phone conversations in Pending status 2021-12-06 22:42:34 -08:00
FreeScout
69cbede2f4 Clarify in comment why customer changes when CCed recipient replies 2021-12-05 21:56:07 -08:00
FreeScout
754c3d7e75 Merge pull request #1575 from Wouter0100/patch-2
Fix Thread type being changed in ThreadObserver
2021-12-06 08:34:04 +03:00
Wouter van Os
3814a2d3e9 Fix issue of Thread type being incorrect after save
This caused all kinds of weird behaviours, where one of them was #1574. This fixes it.
2021-12-05 21:16:20 +01:00
FreeScout
97fb5678bc Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2021-12-03 04:58:12 -08:00
FreeScout
964249c1d4 v1.7.23 2021-12-03 04:57:52 -08:00
FreeScout
607ae55db7 Fix issue with chat convesations being converted into regular conversations - closes #1565, #1568 2021-12-03 04:57:11 -08:00
FreeScout
b7dae29b5f Properly process attachments with slashes - closes #1566 2021-12-03 04:44:40 -08:00
FreeScout
87d48a3428 Fix Trying to get property id of non-object in SendNotificationToUsers.php - closes #1557 2021-12-03 04:38:52 -08:00
FreeScout
82a8876209 replaceAll() JS function 2021-12-02 21:57:00 -08:00
FreeScout
325ad1914b .panel-sortable-input style 2021-12-02 21:56:40 -08:00
FreeScout
d1bcef4df7 Update rollback for remove_unique_index_in_folders_table 2021-12-01 06:08:21 -08:00
FreeScout
7c0ef24bfd loadMailboxes() for convesations and checking status in changeStatus() 2021-12-01 04:12:11 -08:00
Moritz Marquardt
038c3610ac Implement configuration file for default subscriptions (fixes #294) 2021-11-30 19:11:02 +01:00
FreeScout
62ea562f26 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2021-11-30 04:12:54 -08:00
FreeScout
3f17e9b811 v1.7.22 2021-11-30 04:12:30 -08:00
FreeScout
c08c077b8f Properly save attachments without a name - closes #1548 2021-11-30 04:02:51 -08:00
FreeScout
b5efc28b0f Set data-customer_id on customer conversation page to fix deleting customer 2021-11-30 03:37:01 -08:00
FreeScout
ab55a5fe19 Allow multiple personal folders for users - closes #843 2021-11-30 00:57:05 -08:00
FreeScout
f5e493e5dc Fix empty customer when creating a phone conversation without saving it as a draft first 2021-11-29 23:03:51 -08:00
FreeScout
e6ab5eba07 Fix (no subject) issue when creating a new conversation and sending it quickly 2021-11-29 22:45:45 -08:00
FreeScout
d0f1b621fe Fix creating a phone conversation without saving it first as draft 2021-11-29 22:42:33 -08:00
FreeScout
79aca06be8 Allow to get via phone conversation last reply via getLastReply() 2021-11-29 22:42:10 -08:00
FreeScout
9e7f2e22dd Add html_tag parameter to initModals() function 2021-11-29 00:38:23 -08:00
FreeScout
d11c9c83f0 glyphicon-spin 2021-11-19 07:06:59 -08:00
FreeScout
9e0622f91f .footer-hide class 2021-11-17 03:33:47 -08:00
FreeScout
ea7e30b9cc Disable authentication for cron URL - closes #1545 2021-11-17 03:33:33 -08:00
FreeScout
9c52ea8eca Add badges partial 2021-11-17 00:24:05 -08:00
FreeScout
79db2920ca Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2021-11-14 23:13:11 -08:00
FreeScout
df9370a37f v1.7.21 2021-11-14 23:12:55 -08:00
FreeScout
416d07fcce Heading styles 2021-11-14 23:12:30 -08:00
FreeScout
5384a32d58 Set last reply date for phone conversations 2021-11-09 09:28:12 -08:00
FreeScout
18e9679553 Adjust .main-heading style 2021-11-09 08:46:29 -08:00
FreeScout
c0cef1b178 Slovak language added 2021-11-08 03:34:06 -08:00
FreeScout
045c1833e3 Update French translations 2021-11-08 03:30:49 -08:00
FreeScout
d4f8134de4 Update Croatian translations 2021-11-08 03:27:27 -08:00
FreeScout
78596eb4c9 Merge pull request #1528 from andrewlimaza/master
Add support to get module image from modules.json
2021-11-01 16:50:49 +03:00
Andrew Lima
380a275248 Add support to get module image from modules.json
Add support for module.json 'img' attribute. Use this to set a path to the image for your custom module icon/logo.
2021-11-01 14:23:53 +02:00
FreeScout
a0b98cabf1 Fix duplicated customers in the Search - closes #1521 2021-10-29 07:17:51 -07:00
FreeScout
2ccc6dd141 Disable sql_require_primary_key=0 for PostgreSQL - closes #1518 2021-10-28 05:51:19 -07:00
FreeScout
a52336dc01 Fix notifications dropdown height on screen with height > 1086px - closes #1517 2021-10-28 05:37:01 -07:00
FreeScout
d70ae69dae Correct photo size in user profile 2021-10-28 05:25:41 -07:00
FreeScout
5eeed269b6 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2021-10-21 05:54:00 -07:00
FreeScout
eedc9e8f94 v1.7.20 2021-10-21 05:53:42 -07:00
FreeScout
950af7b268 Add Croatian language 2021-10-21 05:50:53 -07:00
FreeScout
5f9e02012a Access user in created_by_user_id in Thread::createExtended() 2021-10-21 05:40:57 -07:00
FreeScout
548517b906 Merge branch 'master' of github.com:freescout-helpdesk/freescout 2021-10-21 00:54:21 -07:00
FreeScout
c29f902483 Fix auto refreshing conversation by new user message - closes #1405 2021-10-21 00:53:44 -07:00
FreeScout
46919ba5a3 Merge pull request #1500 from alinmbb/master
Add side layout hook on the New Conversation page
2021-10-20 18:01:48 +03:00
FreeScout
c4a4f33633 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2021-10-20 07:45:16 -07:00
FreeScout
5176f0456f v1.7.19 2021-10-20 07:44:54 -07:00
alinmbb
fb3e2635f5 Update create.blade.php
Updated hook name
2021-10-20 13:38:49 +02:00
alinmbb
380d1269d0 Update create.blade.php
Added side layout hook on the New Conversation page
2021-10-20 13:33:05 +02:00
FreeScout
069e82c097 users.email filter 2021-10-20 04:26:54 -07:00
FreeScout
9a836a01fd Add password reset throttling 2021-10-20 00:27:07 -07:00
FreeScout
eaffa01452 Add community modules link to the readme 2021-10-19 23:43:45 -07:00
FreeScout
169fa21966 Add cloud hosted link to readme 2021-10-19 23:41:13 -07:00
FreeScout
20e0dae7a3 Add cloud hosted link to readme 2021-10-19 23:39:33 -07:00
FreeScout
358db187be If conversation from deleted customer arrives the customer is empty 2021-10-16 00:50:38 -07:00
FreeScout
fe17e3c4bb Merge pull request #1491 from laurentenhoor/feature/minimal-ui-changes-with-maximal-impact
Set min dashboard card header height
2021-10-16 10:38:35 +03:00
FreeScout
60e0d3c40c Check customer existence in thread tempalte 2021-10-15 23:55:15 -07:00
FreeScout
1753a6f654 customer.edit.before_form hook 2021-10-15 23:54:57 -07:00
FreeScout
d41e0e6c8c Fix protocol in search tabs 2021-10-15 23:54:21 -07:00
FreeScout
6f09d91a50 closeAllModals() JS function 2021-10-15 10:59:44 -07:00
FreeScout
75c6008acc Fix error in SendNotificationToUsers.php when deleting customers 2021-10-15 10:59:32 -07:00
Lauren ten Hoor
7c581cc7bb Another fix for additional dashboard items. 2021-10-15 12:05:50 +01:00
FreeScout
0715f008ca Merge pull request #1490 from laurentenhoor/feature/minimal-ui-changes-with-maximal-impact
Minor UI updated for more professional look and feel.
2021-10-15 12:29:09 +03:00
Lauren ten Hoor
61f4c3760b Minor UI updated for more professional look and feel. 2021-10-15 10:18:50 +01:00
FreeScout
ad1a464335 Add customer menu 2021-10-15 01:16:03 -07:00
FreeScout
76ca9a1153 Merge pull request #1486 from laurentenhoor/bugfix/unable-to-run-isDraft()-on-null
Fixing error about: "unable to run isDraft() on null".
2021-10-15 09:42:08 +03:00
Lauren ten Hoor
461a786ebc Fixing error about: "unable to run isDraft() on null". 2021-10-14 13:38:03 +01:00
FreeScout
170c282f3e Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2021-10-14 03:26:29 -07:00
FreeScout
c7480c81b2 v1.7.18 2021-10-14 03:26:11 -07:00
FreeScout
de62a0c11b Add .eml extension to RFC822 attachments - closes #986 2021-10-13 11:04:30 -07:00
FreeScout
dc35f63586 Fix reopening a conversation when customer replies from chat - closes #1461 2021-10-13 07:31:46 -07:00
FreeScout
f5d7808792 Fix reopening a conversation when customer replies from chat - closes #1461 2021-10-13 01:51:49 -07:00
FreeScout
1bf03b619a Undelete deleted conversation when customer replies via chat or API - closes #1468 2021-10-13 00:55:20 -07:00
FreeScout
e109f5782c Add Danish language 2021-10-12 23:24:02 -07:00
FreeScout
3c57cf0f65 Fix users_to_notify must be an array 2021-10-06 11:05:54 -07:00
FreeScout
475917c110 Log error when app can not fetch message from mail server in Show Original window - closes #1446 2021-10-04 23:22:02 -07:00
FreeScout
3b7ece1790 Web cron - closes #1452 2021-10-04 04:54:02 -07:00
FreeScout
2a6c5faa79 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2021-09-23 03:06:15 -07:00
FreeScout
7446cce387 v1.7.17 2021-09-23 03:05:59 -07:00
FreeScout
15bb477e84 thread.menu.append hook 2021-09-22 05:28:33 -07:00
FreeScout
83071f3a1d Allow to skip user time zone in dateFormat() function 2021-09-22 05:27:32 -07:00
FreeScout
eb579823db Add indexes to Conversations table - closes #1425 2021-09-21 03:12:54 -07:00
FreeScout
fe702bcaea Fix Undefined constant STDIN on module activation - closes #1423 2021-09-21 01:15:44 -07:00
FreeScout
0d87a6ea93 Fix Call to a member function getFullName in web_notifications partial - closes #1410 2021-09-21 00:19:26 -07:00
FreeScout
d9135c134b Update Russian translation 2021-09-21 00:13:13 -07:00
FreeScout
eb33b0c3c7 Update Polish translation 2021-09-21 00:12:15 -07:00
FreeScout
93471f4342 Update German translation 2021-09-21 00:11:00 -07:00
FreeScout
0db0bfdfd4 Improve security of Translate page 2021-08-29 01:47:05 -07:00
FreeScout
92e4dbcaa8 Add SESSION_SECURE_COOKIE to web installer 2021-08-29 00:53:24 -07:00
FreeScout
15443d155b Add SESSION_SECURE_COOKIE to .env.example 2021-08-29 00:50:05 -07:00
FreeScout
21283962c4 Add SECURITY.md 2021-08-29 00:08:39 -07:00
FreeScout
1816aa2761 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2021-08-25 05:53:54 -07:00
FreeScout
3d862b03eb v1.7.16 2021-08-25 05:53:36 -07:00
FreeScout
411fca2ab3 Fix page generation error with multiple agents 2021-08-25 05:24:27 -07:00
FreeScout
8190907e39 Merge pull request #1396 from ejhayter/ejhayter-patch-2
Fix page generation error with multiple agents
2021-08-25 15:05:47 +03:00
Emily Hayter
05fbc0b80d Fix page generation error with multiple agents
When multiple agents were viewing an email, the following error was seen:

usort(): Returning bool from comparison function is deprecated, return
    an integer less than, equal to, or greater than zero at
    /www/html/app/Http/Controllers/ConversationsController.php:252

Opted for spaceship operator, I was not able to verify that the code is
operating as intended, only that it resolved the generated error.
2021-08-25 12:33:29 +01:00
FreeScout
347397f4da Help info for Alternate Emails field 2021-08-20 00:41:44 -07:00
FreeScout
1d0e37950d Allow users to reply from Alternate emails - closes #1363 2021-08-19 07:12:47 -07:00
FreeScout
7828df20fc Add vendor files to dist 2021-08-10 22:19:57 -07:00
FreeScout
a72f6b964f Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2021-08-10 22:18:39 -07:00
FreeScout
950dc05843 v1.7.15 2021-08-10 22:18:18 -07:00
FreeScout
6a242635f7 fzaninotto/faker v1.9.2 2021-08-10 22:14:53 -07:00
FreeScout
dff1bd0d40 Merge pull request #1364 from admdly/fix-faker-php74
Update fzaninotto/faker to fix PHP >= 7.4 compatibility
2021-08-11 08:13:31 +03:00
Adam
d678390b17 Bump fzaninotto/faker to fix PHP >= 7.4 compatibility
Update fzaninotto/faker to fix issue due to changes with implode() in PHP >= 7.4 (see fzaninotto/faker#1748).
2021-08-10 06:06:54 +01:00
FreeScout
579b0bc5ef Fix saving multiple recipients in a new conversation draft 2021-08-06 23:31:07 -07:00
FreeScout
23b01416d7 Fix short text in the menu notification 2021-08-06 07:18:05 -07:00
FreeScout
3266b33b05 Make SVG attachments non-viewable to avoid XSS - closes #1359 2021-08-06 03:12:51 -07:00
FreeScout
ad686553e5 Allow modules to show menu notifications 2021-08-05 23:45:57 -07:00
FreeScout
e289f5acbd Fix short text in the menu notification 2021-08-05 07:05:36 -07:00
FreeScout
1573f73a0e Extend thread description text compilation 2021-08-05 06:52:28 -07:00
FreeScout
efcac585ef Fix summernote click on HintPopover suggestions doesnt insert 2021-08-05 03:34:44 -07:00
FreeScout
89757bc7dd Subscription hooks 2021-08-05 03:25:12 -07:00
FreeScout
04e8391ca2 editor.options hook 2021-08-04 23:47:27 -07:00
FreeScout
7f6bda6c76 Add vendor files to dist 2021-08-03 07:47:45 -07:00
FreeScout
b3b5063b50 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2021-08-03 07:45:49 -07:00
FreeScout
a79d9aa094 v1.7.14 2021-08-03 07:45:35 -07:00
FreeScout
b8f3200f8f Fix htmlpurifier INTL_IDNA_VARIANT_2003 issue on PHP7.2 and 7.3 - closes #1322 2021-08-03 07:30:57 -07:00
FreeScout
b702566cb0 Fix mobile notifications link 2021-08-03 06:26:39 -07:00
FreeScout
69b4d0a322 Fix starred folder position 2021-08-01 04:12:27 -07:00
FreeScout
b2d17e3639 Highlight folders counters 2021-07-31 11:02:15 -07:00
FreeScout
29ef585c31 Folders hooks 2021-07-31 11:00:55 -07:00
FreeScout
d3d1c4ccb9 Highlight folders counters 2021-07-31 05:51:00 -07:00
FreeScout
f53a260dff Add arrow showing conversations sorting column - closes #919 2021-07-30 23:16:19 -07:00
FreeScout
e8e86d1bf4 email.auto_reply.subject hook 2021-07-28 22:49:36 -07:00
FreeScout
284d8f07fa Fix in_array expects parameter 2 to be array in BroadcastNotification 2021-07-28 21:00:57 -07:00
FreeScout
aca1d26a76 Properly restore Cc and Bcc from draft 2021-07-18 05:01:29 -07:00
FreeScout
3ef5c28bac Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2021-07-18 01:37:49 -07:00
FreeScout
de5c9f6047 v1.7.13 2021-07-18 01:37:27 -07:00
FreeScout
b4b93af96a Make users unique in the Assigned search filter - closes #1326 2021-07-17 22:30:59 -07:00
FreeScout
9d52e8853e Conversation recipient selector hooks 2021-07-12 00:22:48 -07:00
FreeScout
2369c9b3be v1.7.12 2021-07-10 22:37:56 -07:00
FreeScout
8ee0699b3e v1.7.12 2021-07-10 22:35:58 -07:00
FreeScout
fcb18e56f0 v1.7.12 2021-07-10 22:32:19 -07:00
FreeScout
14bbab0969 Merge pull request #1319 from fulldecent/patch-5
Add supported versions of PHP
2021-07-11 08:30:23 +03:00
William Entriken
d84fe65460 Add supported versions of PHP 2021-07-09 14:51:48 -04:00
FreeScout
047b8d1433 Merge pull request #1308 from henrikhj/patch-1
Fix tracking pixel in PHP 8
2021-07-07 09:39:32 +03:00
FreeScout
6e699a27ab Merge pull request #1310 from fulldecent/patch-4
Add button role for conversation actions
2021-07-07 09:36:38 +03:00
William Entriken
fd45d20876 Extend aria-labels 2021-07-06 16:16:56 -04:00
William Entriken
f977ff30ea More role and aria label 2021-07-06 16:16:56 -04:00
William Entriken
27946aabae aria-label for dropdowns 2021-07-06 16:16:56 -04:00
William Entriken
acdc935f70 Add aria-label 2021-07-06 16:16:53 -04:00
William Entriken
28cd928a1b Add role buttons 2021-07-06 16:16:44 -04:00
Henrik Jensen
405f09ff00 Fixes #1252
Fixes #1252 "Missing format specifier at end of string" in sprintf due to mismatched number of % and format specifiers.
2021-07-05 15:16:04 +02:00
FreeScout
9c1ac14bf1 Fix duplicating attachments when creating a new conversation 2021-07-03 04:28:58 -07:00
FreeScout
920afd6045 Add Telegram to customer social profiles list 2021-07-03 03:00:54 -07:00
FreeScout
60912da17c Add mailbox name to the title - closes #1297 2021-07-02 23:13:40 -07:00
FreeScout
68eff16f64 Merge pull request #1284 from fulldecent/patch-3
Create FUNDING.yml
2021-07-03 08:12:56 +03:00
William Entriken
475eb0565d Add funding option, fixes #1279 2021-06-24 14:27:57 -04:00
FreeScout
cc58b6739b Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2021-06-23 07:32:51 -07:00
FreeScout
3f66a76727 v1.7.11 2021-06-23 07:32:42 -07:00
FreeScout
deaec751b0 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2021-06-23 07:31:32 -07:00
FreeScout
baff0ab7e4 Fix Array to string converstion error when creating a custommer - closes #1282 2021-06-23 07:28:52 -07:00
FreeScout
45a56f6ca3 Empty alt for logo image 2021-06-22 23:30:30 -07:00
FreeScout
8a14756cea Merge pull request #1278 from fulldecent/patch-1
Fix HTML warning by adding alt to the logo image
2021-06-23 09:28:55 +03:00
FreeScout
c39b0f1e02 Return hyphen to the footer 2021-06-22 23:27:46 -07:00
FreeScout
27d8706023 Merge pull request #1280 from fulldecent/patch-2
Update numeric range dash
2021-06-23 09:24:50 +03:00
FreeScout
ef434e5491 Updated Japanese translations 2021-06-22 23:24:16 -07:00
FreeScout
88c960dfad Italian translation 2021-06-22 23:22:48 -07:00
William Entriken
d028dee724 Update app.blade.php 2021-06-22 11:25:27 -04:00
William Entriken
d17a9e2ef1 Fix HTML warning 2021-06-22 10:23:17 -04:00
FreeScout
85399792fb email.reply_to_customer.subject hook 2021-06-17 00:51:44 -07:00
FreeScout
a02ed17233 Replace calendar partial with include_datepicker 2021-06-16 23:00:31 -07:00
FreeScout
3dc5f1c843 Replace calendar partial with include_datepicker 2021-06-16 10:04:36 -07:00
FreeScout
2a077ecf81 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2021-06-16 05:06:41 -07:00
FreeScout
1ae5fe8cc3 v1.7.10 2021-06-16 05:05:06 -07:00
FreeScout
658ac6b759 Disable updated_at field in Job 2021-06-15 05:18:24 -07:00
FreeScout
08973f9b50 email_notification.before_body hook 2021-06-15 01:49:15 -07:00
FreeScout
1abd3e2aa7 Thread unset meta function 2021-06-15 01:48:43 -07:00
FreeScout
49929dd4a0 Delay hooks 2021-06-15 01:48:28 -07:00
FreeScout
15cce31886 Adjust alerts within thread body 2021-06-11 02:00:32 -07:00
FreeScout
0302556635 thread.before_save_from_request hook 2021-06-11 02:00:17 -07:00
FreeScout
1ae8ff4b99 Init modals after adding editor bottom tollbar to HTML 2021-06-10 06:54:33 -07:00
FreeScout
ca75a702ca conversation.append_send_dropdown hook 2021-06-10 06:54:03 -07:00
FreeScout
14096aeaf8 Fix customer sidebar height with JS 2021-06-10 02:09:23 -07:00
FreeScout
b9ebef7817 Fix customer sidebar height with JS 2021-06-10 01:46:33 -07:00
FreeScout
b4f6f8f2d0 Update PL translation 2021-06-09 01:44:32 -07:00
FreeScout
d27b2f9727 Copy customer email on clicking on it 2021-06-05 03:49:15 -07:00
FreeScout
fd0e58e957 Merge pull request #1248 from foliovision/fix-email-customer-link
Open New Conversation by clicking on customer email
2021-06-05 13:34:48 +03:00
Alec Kinnear
eaf08c60d4 Fixing missing Email Customer link in sidebar
Realized same template is used also for the customer view, where $mailbox cannot be used
2021-06-04 15:24:08 +02:00
Alec Kinnear
6e900d111c Fixing missing Email Customer link in sidebar
Found proper function
2021-06-04 13:55:36 +02:00
Alec Kinnear
cf6a192ccc Fixing missing Email Customer link in sidebar 2021-06-04 13:46:18 +02:00
FreeScout
515a8d2b54 Remove office hours notice from mailbox auto reply page 2021-05-21 22:14:59 -07:00
FreeScout
4018604028 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2021-05-21 07:55:19 -07:00
FreeScout
55d70f8454 Fix encrypting mailbox password migration 2021-05-21 07:54:45 -07:00
FreeScout
fd08b5d4ee v1.7.9 2021-05-21 07:42:05 -07:00
FreeScout
f7cd18d4ad Fix mailbox password encyption migration 2021-05-21 07:41:39 -07:00
FreeScout
078e098b28 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2021-05-21 05:55:25 -07:00
FreeScout
b2f761ef76 1.7.8 2021-05-21 05:54:48 -07:00
FreeScout
cdd894b50c Improve encrypting mail password 2021-05-21 05:54:02 -07:00
FreeScout
323524ac1b Merge pull request #1227 from davask/encrypt_mail_password
encrypt mail password
2021-05-21 15:35:29 +03:00
FreeScout
bc04ca3db7 Improve encrypting out password 2021-05-21 05:32:08 -07:00
FreeScout
434d2c76e0 Merge branch 'master' of github.com:freescout-helpdesk/freescout 2021-05-21 05:28:34 -07:00
FreeScout
009aecc72a Merge pull request #1226 from davask/encrypt_out_password
Encrypt out_password
2021-05-21 15:27:47 +03:00
davask
ddd7885038 encrypt mail password 2021-05-21 12:21:54 +02:00
FreeScout
a46a7f5429 autoreply.should_send 2021-05-21 01:51:11 -07:00
davask
fe5e7011cc Encrypt out_password 2021-05-21 10:22:57 +02:00
FreeScout
e12da9e82d Properly update folders when moving conversation - closes #1210 2021-05-20 02:09:12 -07:00
FreeScout
dddf4d12df Add vendor files to dist 2021-05-18 23:27:40 -07:00
FreeScout
96033cbc78 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2021-05-18 23:26:08 -07:00
FreeScout
b821d3fd09 v1.7.7 2021-05-18 23:25:19 -07:00
FreeScout
1b6dcb00e7 mailbox.url hook 2021-05-18 08:03:49 -07:00
FreeScout
938e4e1ad7 folder.url hook 2021-05-18 07:44:31 -07:00
FreeScout
cd99b04507 conversations.ajax_pagination_folder hook 2021-05-18 06:36:55 -07:00
FreeScout
ba49cde9f6 folder.active_count_mine_folder hook 2021-05-18 06:35:41 -07:00
FreeScout
3f82e25074 Cache starred conversations per mailbox 2021-05-18 00:58:19 -07:00
FreeScout
f132f88059 Improve conversations pagination 2021-05-18 00:54:16 -07:00
FreeScout
38ff9e748a Allow to pass params to conversations table template 2021-05-17 23:05:50 -07:00
FreeScout
c299e0c3e2 mailbox.sidebar_folder_url hook 2021-05-17 23:05:26 -07:00
FreeScout
d4127889c3 Filter to disable mailbox sidebar buttons 2021-05-17 06:01:43 -07:00
FreeScout
994ead0e3e Hook after mailbox sidebar - closes #1209 2021-05-17 05:54:15 -07:00
FreeScout
3e5900b1e0 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2021-05-14 11:20:54 -07:00
FreeScout
f579c43b29 v1.7.6 2021-05-14 11:19:56 -07:00
FreeScout
f2fee6779d thread.attachments_list_append hook 2021-05-14 10:42:54 -07:00
FreeScout
4abb7ecc46 Improved createZipArchive() function 2021-05-14 10:37:16 -07:00
FreeScout
7e8fa56bf5 Add link to remind license keys 2021-05-11 07:13:25 -07:00
FreeScout
ce02ba86db Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2021-05-11 02:46:23 -07:00
FreeScout
7fcb585268 v1.7.5 2021-05-11 02:45:24 -07:00
FreeScout
e143280c6c getFileContents() function for Attachment 2021-05-11 02:42:52 -07:00
FreeScout
f82d36afb0 Improve no_activations_left message 2021-05-09 10:33:37 -07:00
FreeScout
e14d899ac4 Fix createExtended Thread method 2021-04-30 10:30:03 -07:00
FreeScout
6a16cbabd2 Fix search in PostgreSQL - closes #1174 2021-04-29 01:39:31 -07:00
FreeScout
e52711d592 Process incoming email even if imap_mime_header_decode fails - closes #351 2021-04-29 00:38:21 -07:00
FreeScout
abc01538f9 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2021-04-23 00:13:23 -07:00
FreeScout
6eaf4359b5 v1.7.4 2021-04-23 00:13:14 -07:00
FreeScout
728959332b Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2021-04-23 00:11:58 -07:00
FreeScout
d58a0981c4 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2021-04-22 22:47:21 -07:00
FreeScout
23cdcffb12 v1.7.3 2021-04-22 22:46:42 -07:00
FreeScout
944363765a Update readme 2021-04-22 00:09:13 -07:00
FreeScout
ace3bdbb8f Add State filter to search to filter Deleted conversations - closes #912 2021-04-21 06:16:23 -07:00
FreeScout
7926b9fd0f Compare exact conversation number and id when searching - closes #1174 2021-04-21 05:11:28 -07:00
FreeScout
4617b34478 Fix showing floating alert after sending a reply - closes #1119 2021-04-21 04:56:53 -07:00
FreeScout
7fcda1f0e7 Update readme 2021-04-21 02:08:36 -07:00
FreeScout
a72842cafe Update hit counter code in readme 2021-04-21 01:57:51 -07:00
FreeScout
8ffbfd5653 Update German translation 2021-04-21 01:47:01 -07:00
FreeScout
955681886f Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2021-04-15 05:40:55 -07:00
FreeScout
066656adf8 Update readme 2021-04-15 05:38:47 -07:00
FreeScout
a5db2a49a8 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2021-04-15 00:59:45 -07:00
FreeScout
4a5dd111d7 v1.7.2 2021-04-15 00:57:40 -07:00
FreeScout
73d260c2db Add meta to Customer 2021-04-15 00:57:02 -07:00
FreeScout
c86e895ea9 thread.body_output hook 2021-04-14 23:49:22 -07:00
FreeScout
208bdb28fe Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2021-04-14 00:10:38 -07:00
FreeScout
70b3b2f75b v1.7.1 2021-04-14 00:09:38 -07:00
FreeScout
8f35603493 Rename extra hooks in conversations table 2021-04-14 00:05:22 -07:00
FreeScout
1c592e6713 Merge pull request #1149 from droogadulce/master
Add hooks to conversations table
2021-04-14 10:02:02 +03:00
FreeScout
b0d22fc035 Merge branch 'master' of github.com:freescout-helpdesk/freescout 2021-04-14 00:00:08 -07:00
FreeScout
7f5c9dd4ef Allow to override system email footer 2021-04-13 05:41:34 -07:00
FreeScout
76e2a3ef46 Do not try to send auto reply by email when Chat/Messenger message received - closes #1151 2021-04-13 05:11:29 -07:00
FreeScout
9c48b0054f Update PL translaiton 2021-04-13 01:32:50 -07:00
droogadulce
72db97df79 Hook to show followers in the mailbox dashboard 2021-04-09 20:52:32 -05:00
FreeScout
a8b8b49dce Merge pull request #1131 from fulldecent/dist
Occurend -> occured, fixes #1130
2021-04-09 09:22:50 +03:00
William Entriken
26f9939bff Fix typo: occurend -> occurred 2021-04-04 20:10:51 -04:00
FreeScout
37ec94c157 Merge pull request #1136 from droogadulce/translations
Update Spanish translations
2021-04-03 07:51:30 +03:00
droogadulce
b336b2e54e Update Spanish translations 2021-04-02 19:35:17 -06:00
FreeScout
91a0cecffa Add vendor files to dist 2021-03-31 00:51:49 -07:00
FreeScout
a59204cd99 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2021-03-31 00:37:55 -07:00
FreeScout
cd61c3fd8d v1.7.0 2021-03-31 00:36:53 -07:00
FreeScout
34082cb122 PHP 8 support - closes #1002 2021-03-31 00:35:40 -07:00
FreeScout
70d92cef40 Fix readme hits counter 2021-03-25 00:00:55 -07:00
FreeScout
b3c1a97f3d Update Japanese translation 2021-03-24 23:22:56 -07:00
FreeScout
f3aa63aa13 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2021-03-17 01:33:23 -07:00
FreeScout
27c8753b3a v1.6.20 2021-03-17 01:32:22 -07:00
FreeScout
d352007cd2 Merge pull request #1101 from tillmon/master
Fix prefill_to
2021-03-17 11:27:08 +03:00
FreeScout
3fe95f4362 Allow customer options in fsAjax function 2021-03-16 10:24:37 -07:00
FreeScout
f13a201c56 Allow to pass UploadedFile as attachment when creating a Thread 2021-03-16 10:24:17 -07:00
tillmon
d4ca8c77b5 Fix prefill_to 2021-03-16 08:16:38 +01:00
FreeScout
920690e504 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2021-03-13 22:21:03 -08:00
FreeScout
282eebce76 v1.6.19 2021-03-13 22:19:45 -08:00
FreeScout
abeddf8c65 binaryDataMimeType() helper function 2021-03-13 22:18:47 -08:00
FreeScout
826cafc3a9 Return data in create() functions 2021-03-13 02:18:38 -08:00
FreeScout
04422376f2 Send full email history when forwardning global settings is non - closes #1087 2021-03-10 22:40:05 -08:00
FreeScout
f88f805f28 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2021-03-07 09:07:55 -08:00
FreeScout
bfe4ec2818 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2021-03-07 09:01:40 -08:00
FreeScout
9e07d636a8 v1.6.18 2021-03-07 09:00:59 -08:00
FreeScout
b10211dcf9 Update Polish translation 2021-03-07 08:59:24 -08:00
FreeScout
9a7c88efbf mail.process_swift_message header 2021-03-07 08:48:11 -08:00
FreeScout
a0750dddfc Outgoing emails type header 2021-03-07 08:47:47 -08:00
FreeScout
90b0100048 Show more detailed error when module manifest file can not be cached 2021-03-05 00:40:03 -08:00
FreeScout
bbb5763b8c Play audio attachments in browser 2021-03-03 23:40:54 -08:00
FreeScout
b029070d94 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2021-03-03 03:41:42 -08:00
FreeScout
499b2dc748 v1.6.17 2021-03-03 03:40:27 -08:00
FreeScout
153f1cbd31 Update German translation 2021-03-03 03:23:53 -08:00
FreeScout
9247f96679 Helper remoteFileName() function 2021-03-03 03:23:23 -08:00
FreeScout
d18d138f73 Include images links into plain text emails 2021-03-03 01:58:41 -08:00
FreeScout
36f0686cf4 Improve video attachment type detection 2021-03-03 00:08:10 -08:00
FreeScout
963e637931 Conversation subjectFromText() function 2021-03-02 23:45:06 -08:00
FreeScout
570d5bc60b Disable merging and fowarding for chat conversations 2021-03-02 04:50:18 -08:00
FreeScout
4c37af4976 Allow only plain text in chat conversations 2021-03-02 04:25:31 -08:00
FreeScout
f09fbdba72 Send chat conversation reply 2021-03-02 04:10:13 -08:00
FreeScout
17cd4b0460 Clean up chat conversation reply area 2021-03-02 02:17:03 -08:00
FreeScout
02e234a627 Show new message alert in the title when new message arrives 2021-03-02 01:13:53 -08:00
FreeScout
79c13c7f31 Show immediately new chat messages 2021-03-01 23:57:07 -08:00
FreeScout
c86ae98d04 Do not allow to change chat conversation customer 2021-03-01 23:03:18 -08:00
FreeScout
aec129f6bb Show customer channel as tag 2021-03-01 23:00:31 -08:00
FreeScout
b7d6e06530 Show conversation channel tags 2021-03-01 22:50:19 -08:00
FreeScout
d1fc654e36 Functions to create Conversation and Thread 2021-03-01 22:07:19 -08:00
FreeScout
1e99dbe589 Customer setPhotoFromRemoteFile() function 2021-03-01 06:20:00 -08:00
FreeScout
80a5f6188b Add channel columns to Customer and Conversation 2021-03-01 06:19:19 -08:00
FreeScout
265728dc38 Add gmail_quote to the outgoing emails to customers to let other systems better separate replies 2021-02-26 02:03:20 -08:00
FreeScout
863a29dbe6 Fix an issue with sending multiple duplicate emails #870, #1041 2021-02-26 00:39:47 -08:00
FreeScout
b28330ed56 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2021-02-25 07:05:44 -08:00
FreeScout
4ec7666985 1.6.16 2021-02-25 07:04:26 -08:00
FreeScout
63cf643300 Deactivate a module when its folder renamed 2021-02-25 06:44:43 -08:00
FreeScout
85c167a5f4 Update JA translation 2021-02-24 23:40:44 -08:00
FreeScout
1f4fbbd5f8 Update RU translation 2021-02-23 23:12:56 -08:00
FreeScout
d9405428d6 Fix invitation email text - closes #1066 2021-02-23 23:12:33 -08:00
FreeScout
91280c2177 Fix Row size too large error on adding meta column to mailboxes - closes #1058 2021-02-17 09:28:19 -08:00
FreeScout
9abba00fe1 Readme update 2021-02-12 21:24:02 -08:00
FreeScout
cd2b36e5c7 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2021-02-12 21:18:56 -08:00
FreeScout
8552d7e825 v1.6.15 2021-02-12 21:18:08 -08:00
FreeScout
5e1e9399cc Japanese language added 2021-02-12 21:13:45 -08:00
FreeScout
6a5e4af0f3 Update readme 2021-02-12 06:18:06 -08:00
FreeScout
38b31fce3c Allow to attach files in the mailbox signature 2021-02-12 04:24:56 -08:00
FreeScout
c5fa140d07 Clarification on Translate page 2021-02-12 01:32:00 -08:00
FreeScout
e13f1cf73a Update readme 2021-02-11 22:41:10 -08:00
FreeScout
03ff3f7257 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2021-02-10 04:02:01 -08:00
FreeScout
4c990f43a7 v1.6.14 2021-02-10 04:01:07 -08:00
FreeScout
75497aa031 downloadRemoteFileAsTmpFile() function 2021-02-10 04:00:15 -08:00
FreeScout
a8c69d1cec Adjust navbar brand text 2021-02-09 22:44:42 -08:00
FreeScout
dc4c1b9b2f Determine number of changed translations when publishing and sending 2021-02-09 05:02:57 -08:00
FreeScout
1ef654578e Optimize Translations page 2021-02-09 04:51:17 -08:00
FreeScout
a610fe2fd2 fr and pt-PT translations 2021-02-09 00:55:15 -08:00
FreeScout
97cbb8a03f Update polish translations 2021-02-08 23:41:36 -08:00
FreeScout
0e28127851 .accordion-disabled style 2021-02-08 21:49:07 -08:00
FreeScout
c7c41805d8 Fix highlighting selected top menu item 2021-02-05 23:54:01 -08:00
FreeScout
6cef45d3bd Mailbox setMetaParam method 2021-02-05 23:01:01 -08:00
FreeScout
ded05a5408 Add meta column to mailboxes table 2021-02-05 21:37:50 -08:00
FreeScout
361a4cc242 getGlobalEntity() helper method 2021-02-05 04:59:30 -08:00
FreeScout
6cfed6c229 Update readme 2021-02-03 06:01:20 -08:00
FreeScout
e8d62ab190 Update readme 2021-02-03 05:59:06 -08:00
FreeScout
7a027c4149 Tools to readme 2021-02-03 05:56:34 -08:00
FreeScout
425de91c57 Show email header in plain text emails 2021-01-29 22:50:51 -08:00
FreeScout
d50aef3832 Merge pull request #1023 from fvdm/fix-730
Fixed custom reply separator - closes #730
2021-01-30 09:11:20 +03:00
Franklin
afe2b1b8ce Fixed syntax typo 2021-01-29 01:56:21 +01:00
Franklin
566fff6c0c Fallback to default reply separator if not set 2021-01-29 01:40:09 +01:00
Franklin
88b9c926c4 Fixed custom reply separator 2021-01-28 20:19:02 +01:00
FreeScout
2db13a7ec1 Fix customer setData issue on importing - closes #1013 2021-01-24 08:48:15 -08:00
FreeScout
4d6b740f30 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2021-01-16 04:50:17 -08:00
FreeScout
9a8025777b v1.6.13 2021-01-16 04:49:40 -08:00
FreeScout
715bc2ecbd Use divs in emails to customers instead of tables - closes #923 2021-01-16 04:48:39 -08:00
FreeScout
e05c456f99 Update User savePhoto() function 2021-01-16 00:21:08 -08:00
FreeScout
3c4459bbac downloadRemoteFileAsTmp() helper function 2021-01-16 00:20:46 -08:00
FreeScout
cd128c5683 Merge pull request #1001 from TimoStramann/patch-1
Fix German translation
2021-01-15 12:34:15 +03:00
Timo Stramann
b4cfec133d Fix German translation
Correct wrongly translated strings
2021-01-15 09:51:29 +01:00
FreeScout
408c5351bf Do not send multiple emails to customers when saving outgoing reply to IMAP folder fails - closes #984 2021-01-14 23:58:14 -08:00
FreeScout
6fd295211b Update pt-PT translation 2021-01-14 23:29:49 -08:00
FreeScout
6e9db6d7c9 Update German translation 2021-01-14 23:28:40 -08:00
FreeScout
cd4964401f Make conversations table in customer profile wider 2021-01-07 02:31:52 -08:00
FreeScout
7572febd8d Improve .navbar-brand-with-text vertical alignment 2021-01-06 00:14:26 -08:00
FreeScout
90180b890d Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2020-12-31 00:32:56 -08:00
FreeScout
1e6b0ac6a9 1.6.12 2020-12-31 00:32:02 -08:00
FreeScout
6ab9b5fba7 Do not show empty To field in thread 2020-12-30 05:11:07 -08:00
FreeScout
b9cbf749be Do not send notifications on imported conversations 2020-12-30 03:18:11 -08:00
FreeScout
ea7de1c085 Do not hightlight dashboard folders on hover 2020-12-29 22:12:09 -08:00
FreeScout
eaa681dfc3 Show customer data on a new line 2020-12-29 01:47:04 -08:00
FreeScout
2d79c3df9f Update Polish translations 2020-12-24 23:25:56 -08:00
FreeScout
649134bd22 Increase a bit logo size 2020-12-24 22:48:59 -08:00
FreeScout
abf3192476 Settings stored in .env file no being updated after clicking Save button 2020-12-24 02:22:22 -08:00
FreeScout
b7a2312994 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2020-12-23 23:42:30 -08:00
FreeScout
4d6d16a58b v1.6.11 2020-12-23 23:41:47 -08:00
FreeScout
4d389dcfa3 Highlight headers in the dashboard 2020-12-23 23:37:42 -08:00
FreeScout
01fa48df18 menu_right.user.after_profile hook 2020-12-23 21:54:36 -08:00
FreeScout
3fedf57b72 Improve fetch_emails.unseen hook 2020-12-23 21:52:12 -08:00
FreeScout
3140c2e90c Merge pull request #959 from tillmon/unseen
Fetch emails hook unseen
2020-12-24 08:45:27 +03:00
FreeScout
5fbab18684 Permission to allow users manage users 2020-12-22 23:48:15 -08:00
Tillmann Baumgart
1fc7c4d53d fetch emails hook unseen 2020-12-23 08:16:57 +01:00
FreeScout
2f14216e49 User Permissions per User - closes #741 2020-12-22 10:07:42 -08:00
FreeScout
ae602a2f6f Hide Delete Conversation button if user does not have permissions 2020-12-22 02:26:20 -08:00
FreeScout
08c701a3eb Store user permissions in .env instead of Options 2020-12-22 02:22:35 -08:00
FreeScout
40cebf61c2 Delete send logs older 6 months 2020-12-21 07:04:11 -08:00
FreeScout
c6fa18fe8d Support RTL languages with HTML dir attribute - closes #924 2020-12-21 04:09:54 -08:00
FreeScout
3598d5efdf Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2020-12-16 23:40:59 -08:00
FreeScout
0b305ec22f v1.6.10 2020-12-16 23:40:20 -08:00
FreeScout
fe694de119 Improve set closed_at if conversation is closed after reply 2020-12-16 23:37:16 -08:00
FreeScout
74bb03f92e Merge pull request #941 from tillmon/dev2
Set closed_at if conversation is closed after reply
2020-12-17 10:18:58 +03:00
FreeScout
3446abd7d8 Improve prefilling To address in new conversation 2020-12-16 23:14:38 -08:00
FreeScout
ed0cc6c9be Merge branch 'master' of github.com:freescout-helpdesk/freescout 2020-12-16 23:10:35 -08:00
FreeScout
ef8d6ddf07 customer.profile_preview_prepend hook 2020-12-16 23:10:30 -08:00
FreeScout
3a3dd05393 Adjust transparent button color 2020-12-16 23:10:18 -08:00
FreeScout
f6417a3bd5 Merge pull request #940 from tillmon/dev
Set "to" in new conversation with url parameter
2020-12-17 10:08:35 +03:00
Tillmann Baumgart
49df4425ea set closed_at if conversation is closed after reply 2020-12-16 22:39:07 +01:00
Tillmann Baumgart
758129d035 set "to" in new conversation with url parameter 2020-12-16 22:38:06 +01:00
FreeScout
608e6e7a1d Hightlight new Unassigned and Mine conversations in the dashboard 2020-12-16 01:20:45 -08:00
FreeScout
6cffdba578 Adjust transparent button color 2020-12-16 00:52:43 -08:00
FreeScout
9b64cc270a Condense dashboard content 2020-12-16 00:45:30 -08:00
FreeScout
cc5526f7f3 Adjust logo brightness on hover 2020-12-16 00:39:35 -08:00
FreeScout
77ed94a2e8 Fix deactivating licenses 2020-12-05 10:19:38 -08:00
FreeScout
54657e5dc7 Fix deactivating licenses 2020-12-05 09:59:45 -08:00
FreeScout
62bc3dd614 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2020-12-03 03:18:52 -08:00
FreeScout
ec7f10d119 v1.6.9 2020-12-03 03:17:54 -08:00
FreeScout
50f22ccf25 Allow encoded settings using env_encode parameter 2020-12-03 02:24:43 -08:00
FreeScout
9ae4a5053a Properly separate replies from FreeScout to FreeScout - closes #765 2020-12-02 22:32:32 -08:00
FreeScout
807bddeace Show conversation status icon in the list 2020-12-02 06:11:25 -08:00
FreeScout
74ce6ca353 Show user name when deleting a user 2020-12-02 05:49:24 -08:00
FreeScout
cb10380498 Make customer messages editable - closes #896 2020-12-02 05:39:56 -08:00
FreeScout
490f850834 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2020-11-26 01:47:12 -08:00
FreeScout
9bd75acdba v1.6.8 2020-11-26 01:46:19 -08:00
FreeScout
e8e8bd3262 Fix displaying fields in customer profile 2020-11-26 01:08:21 -08:00
FreeScout
55411f7979 Customer hooks 2020-11-25 06:11:45 -08:00
FreeScout
40e8d39f75 Properly process arrays in getQueryParam() 2020-11-25 06:11:02 -08:00
FreeScout
bd81680cdb Fix notifications dropdown in German language - closes #874 2020-11-24 05:15:02 -08:00
FreeScout
fa557969df Do not fill automatically CC or BCC when forwarding - closes #877 2020-11-24 04:40:05 -08:00
FreeScout
2a5f92eaf2 Custom 403 page 2020-11-24 04:29:34 -08:00
FreeScout
7cd5dd9929 Allow only admin to manage mailbox connection settings - closes #881 2020-11-24 04:22:49 -08:00
FreeScout
a6f18efbc7 Update German translations 2020-11-24 04:13:07 -08:00
FreeScout
fedaa33741 conversation.view.focus event 2020-11-21 22:15:57 -08:00
FreeScout
1eadd06f9d customer.edit.after_fields hook 2020-11-20 22:47:15 -08:00
FreeScout
4f1c2731bf Trigger conversation.view.start when focus returns to conversation 2020-11-20 22:41:51 -08:00
FreeScout
00b52794aa Remove conversation viewer when conversation window is not in focus 2020-11-20 21:27:39 -08:00
FreeScout
f9c2799526 Remove conversation viewer when conversation window is not in focus 2020-11-20 21:15:21 -08:00
FreeScout
bd1278aab4 Show customer social profiles 2020-11-19 03:13:17 -08:00
FreeScout
4f8063214f Rename customer background into notes, delete age, gender fields 2020-11-19 01:15:00 -08:00
FreeScout
fcdf2fba0e Allow to add customer photo 2020-11-18 23:23:55 -08:00
FreeScout
a8479fb315 Show customer social profiles 2020-11-18 23:09:18 -08:00
FreeScout
9fc473efb1 Remove nginx client_max_body_size from system status as it can not be retrieved 2020-11-18 20:45:02 -08:00
FreeScout
712785dc44 New fields added to customer profiles 2020-11-18 06:11:01 -08:00
FreeScout
f904057944 Pagination in customers search 2020-11-18 02:05:43 -08:00
FreeScout
4cc750ec0f Improve conversations navigation 2020-11-18 01:31:21 -08:00
FreeScout
f97f43f49b Misc updates in customers 2020-11-18 00:42:05 -08:00
FreeScout
826e42085c Update search 2020-11-17 21:59:24 -08:00
FreeScout
4e555f3198 Allow embed mode using x_embed parameter 2020-11-17 21:57:55 -08:00
FreeScout
df5d23abbc Pass x_ parameters globally, xs_ parameters only on the same page 2020-11-17 21:56:55 -08:00
FreeScout
8041f6b268 Do not redirect user to the anaccessible mailbox after moving conversations 2020-11-16 06:32:48 -08:00
FreeScout
f22e2d6d93 Update German translation 2020-11-16 05:46:37 -08:00
FreeScout
d770b218f2 Improve attachment download link 2020-11-16 05:07:14 -08:00
FreeScout
93d640206f Merge pull request #871 from Filoz/patch-1
Add download button to attachment
2020-11-16 16:04:42 +03:00
Filippo Lazzarini
19d66a3cb2 Update thread_attachments.blade.php 2020-11-16 10:22:22 +01:00
Filippo Lazzarini
9819668246 Add download button to attachment 2020-11-16 10:18:51 +01:00
FreeScout
944a29814b Show PHP upload_max_filesize / post_max_size 2020-11-15 07:12:13 -08:00
FreeScout
b09ef174ab Do not show deleted users when creating a new mailbox - closes #866 2020-11-15 06:56:28 -08:00
FreeScout
aebf33c2f0 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2020-11-07 04:09:16 -08:00
FreeScout
5e59b7b561 v1.6.7 2020-11-07 04:08:26 -08:00
FreeScout
f47efb8f5f Customer parseName() function 2020-11-07 00:36:56 -08:00
FreeScout
c562b9525a Customer parseName() function 2020-11-06 05:47:38 -08:00
FreeScout
06593afb95 .navbar-brand-with-text style 2020-11-06 05:47:23 -08:00
FreeScout
9d09869f80 localStorageGetObject() and localStorageSetObject() JS functions 2020-11-06 05:47:05 -08:00
FreeScout
32ecbfc718 Make foreign keys fields unsigned - closes #853 2020-11-04 22:41:11 -08:00
FreeScout
ae6fdeef29 Fix starring conversations - closes #662 2020-11-04 05:47:04 -08:00
FreeScout
bbd0cdee78 Check non-writable cache files in System Status 2020-10-31 04:37:41 -07:00
FreeScout
76ab984e0b Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2020-10-31 00:07:11 -07:00
FreeScout
c61c9c86c8 v1.6.6 2020-10-31 00:06:33 -07:00
FreeScout
90e29f8b43 Check non-writable cache files in System Status 2020-10-31 00:04:57 -07:00
FreeScout
e7ab1e4296 .input-group-addon-grey style 2020-10-30 01:09:41 -07:00
FreeScout
e5017a2031 Fix showing Settings updated alert 2020-10-29 22:59:11 -07:00
FreeScout
02ff35a306 Show number of users in Users 2020-10-29 02:38:14 -07:00
FreeScout
c3b90d7952 Fix recipient name on forwarding - closes #786 2020-10-28 23:49:54 -07:00
FreeScout
f9ba58c6f1 Change Remove Formatting button icon 2020-10-28 23:13:43 -07:00
FreeScout
69da2dbd18 Allow headers when viewing threads 2020-10-28 08:02:55 -07:00
FreeScout
308b4a9ecd Allow pre tags when viewing threads 2020-10-28 07:57:09 -07:00
FreeScout
1256f02c13 Change Remove Formatting button icon 2020-10-28 04:50:32 -07:00
FreeScout
b3ee050eb3 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2020-10-27 23:49:58 -07:00
FreeScout
ead26a92c4 v1.6.5 2020-10-27 23:49:24 -07:00
FreeScout
2e4dca4d89 In user notification use the username part of the email address if customer has no name - closes #749 2020-10-27 23:14:23 -07:00
FreeScout
b4d1e3eaff Add quotes when settings .env vars 2020-10-27 22:40:41 -07:00
FreeScout
8a5fa4f158 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2020-10-27 02:39:06 -07:00
FreeScout
fcacca3495 v1.6.4 2020-10-27 02:38:16 -07:00
FreeScout
6e71478598 Fix Unknown system variable sql_require_primary_key - closes #834 2020-10-27 02:37:38 -07:00
FreeScout
e982a8a799 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2020-10-26 02:18:10 -07:00
FreeScout
b591947fac v1.6.3 2020-10-26 02:16:53 -07:00
FreeScout
6ed171fc8f Footer hook 2020-10-26 01:39:48 -07:00
FreeScout
48f68f82dd Escape settings containing quotes 2020-10-26 01:39:34 -07:00
FreeScout
70fbc0c24a Fix insertVar option in summernoteInit() 2020-10-26 01:39:15 -07:00
FreeScout
27049809ef Forwarding to multiple recipients - closes #825 2020-10-25 23:48:14 -07:00
FreeScout
5b4ff8b5c2 include_attachments parameter for Conversation forward() function 2020-10-25 00:04:19 -07:00
FreeScout
0f581ad9cb Check connection only for SMTP send method - closes #828 2020-10-24 07:39:40 -07:00
FreeScout
edf9163ca1 Show full date when printing a conversation - closes #783 2020-10-24 02:37:32 -07:00
FreeScout
83b6e82c1d Update pt-PT translation 2020-10-24 02:29:44 -07:00
FreeScout
c0e54d44c4 Fix File name too long - closes #806 2020-10-24 02:25:46 -07:00
FreeScout
03f20e7ec7 Fix double image on print screen - closes #694 2020-10-24 01:04:58 -07:00
FreeScout
c1e9d01099 Fix sql_require_primary_key issue - closes #818 2020-10-24 00:04:02 -07:00
FreeScout
0f4f2674f7 Fix sql_require_primary_key issue - closes #818 2020-10-23 11:39:42 -07:00
FreeScout
6e752bba1c Merge branch 'master' of github.com:freescout-helpdesk/freescout 2020-10-23 06:12:59 -07:00
FreeScout
077d6ba8a4 Merge pull request #792 from Servery/patch-3
Update de.json (2nd try)
2020-10-23 16:12:28 +03:00
FreeScout
0e3b9d9c1d Update readme 2020-10-07 10:23:10 -07:00
Tim Schulenburg
21edb60c16 Update de.json (2nd try)
Line 201: Missing ":" to display german text with a href correct.
Line 358: Old translation does not sound good in german.
Line 727 - 747: Added some translations
2020-10-07 17:21:46 +02:00
FreeScout
b38cd59b42 Update readme 2020-10-06 02:28:13 -07:00
FreeScout
fbf88be324 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2020-10-06 01:46:01 -07:00
FreeScout
cfa228083d v1.6.2 2020-10-06 01:45:22 -07:00
FreeScout
b9b36e31d2 Merge pull request #788 from injektion/permissions-scroll
make permissions manager scrollable on mobile screens
2020-10-06 11:41:37 +03:00
FreeScout
f25259e78b iOS app link to the Notifications settings 2020-10-06 01:10:25 -07:00
FreeScout
7e24125fe3 iOS to readme 2020-10-06 01:07:34 -07:00
FreeScout
e237f76b0f Remove api routes 2020-10-06 00:27:32 -07:00
FreeScout
a5c9a8cea6 Improve customer creation function 2020-10-06 00:26:50 -07:00
M@
7dca17f2aa make permissions manager scrollable on mobile screens 2020-10-05 13:43:37 -07:00
FreeScout
2befc5c172 Helper::isDev() function 2020-10-04 10:33:08 -07:00
FreeScout
8a667cb8a2 Run second migration after the module has been activated - closes #773 2020-10-02 04:53:12 -07:00
FreeScout
fde1e96104 Do not cut out regular Gmail quotes - closes #778 2020-10-02 01:18:13 -07:00
FreeScout
13ad09022b Fix in_array() expects parameter 2 to be array in BroadcastNotification - closes #761 2020-10-02 01:00:47 -07:00
FreeScout
7c248803dd Do no show Disable User checkbox to non-admins 2020-10-02 00:24:31 -07:00
FreeScout
b4b440f871 Hide New Mailbox button from non-admins 2020-10-02 00:21:42 -07:00
FreeScout
86c29a89c2 Added proc_open to the list of required functions in System Status 2020-10-02 00:19:15 -07:00
FreeScout
ac3255efb5 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2020-10-02 00:08:30 -07:00
FreeScout
24acd8e2fe v1.6.1 2020-10-02 00:07:43 -07:00
FreeScout
a243d7a0ff Printing conversations, messages and notes - closes #507 2020-09-28 00:58:34 -07:00
FreeScout
530715af17 Merging conversations to readme 2020-09-27 23:02:15 -07:00
FreeScout
10e2c0c20f Add outlook reply separator - closes #461 2020-09-27 07:38:27 -07:00
FreeScout
17b1de0825 Hide prev next arrows on small screen 2020-09-27 05:56:37 -07:00
FreeScout
882478edb1 Import email into multiple mailboxes if needed - closes #480 2020-09-27 05:07:32 -07:00
FreeScout
a5e3343f06 Improve Mailbox managers implementation - closes #490 2020-09-25 03:00:54 -07:00
FreeScout
168e4427dc Merge pull request #756 from injektion/mailbox-managers
Issue 490: Mailbox Managers and per user management permissions
2020-09-25 13:00:30 +03:00
FreeScout
069a44774e Merge pull request #760 from cloudron-io/disable-updates
WIP: Add config variable to disable update checks
2020-09-24 13:55:48 +03:00
Johannes Zellner
e67d60d088 WIP: Add config variable to disable update checks 2020-09-24 12:28:27 +02:00
M@
d1239e2e22 issue 490 - Mailbox managers with per user settings 2020-09-22 16:51:25 -07:00
FreeScout
3e1ee77c14 Merge pull request #755 from fvdm/patch-3
Added Search filters to NL translation
2020-09-22 17:40:26 +03:00
FreeScout
973f1386d0 Merge pull request #754 from fvdm/patch-2
Allow translation of search filters
2020-09-22 17:39:11 +03:00
FreeScout
077ed36a4a Merge pull request #751 from fvdm/patch-1
Added Merge strings to NL translation
2020-09-22 17:38:35 +03:00
Franklin
1841ee32f6 Added Search filters to NL translation 2020-09-22 15:50:58 +02:00
Franklin
d16a1938c2 Allow translation of search filters 2020-09-22 15:32:32 +02:00
Franklin
28f3c3be62 Added Merge strings to NL translation 2020-09-22 15:14:00 +02:00
FreeScout
1d2e0fed0e Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2020-09-22 02:01:52 -07:00
FreeScout
065048ee46 v1.6.0 2020-09-22 02:00:55 -07:00
FreeScout
465a982080 Merge conversations - closes #226 2020-09-22 00:59:23 -07:00
FreeScout
8827fae5f3 Edit conversation subject - closes #43 2020-09-21 02:29:12 -07:00
FreeScout
3461a2d36c Added search shortcut: All from current mailbox - closes #591 2020-09-21 01:47:05 -07:00
FreeScout
19891a39e4 Add Conversation History to Send button dropdown - closes #744 2020-09-21 01:17:37 -07:00
FreeScout
b751959f42 Send full conversation history when forwarding conversation via Workflow - closes #743 2020-09-20 07:07:01 -07:00
FreeScout
721a599ed3 Merge branch 'master' of github.com:freescout-helpdesk/freescout 2020-09-17 05:09:52 -07:00
FreeScout
9caf10774b Merge pull request #737 from fvdm/issue-735
Convert subject to UTF-8 encoding
2020-09-17 15:09:34 +03:00
FreeScout
b1dd37025f Require curl extension 2020-09-17 05:06:09 -07:00
FreeScout
9b49a321bb Add shell_exec and ps to System Status page 2020-09-17 05:02:55 -07:00
Franklin
f8b0214519 Convert all subject encodings
and continue parsing on grammar error
2020-09-17 13:26:02 +02:00
Franklin
4bcc9dbc3a Optimized subject encoding conversion 2020-09-17 12:52:57 +02:00
FreeScout
4b44f3c718 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2020-09-16 10:32:29 -07:00
FreeScout
230ce5e405 v1.5.15 2020-09-16 10:31:37 -07:00
FreeScout
ca2bc58881 getActionTypeName() function for thread 2020-09-16 10:27:48 -07:00
FreeScout
8271ccb005 Allow empty folder parameter in getWaitingSince() 2020-09-16 02:36:23 -07:00
FreeScout
e766a9a581 customer.updated hook 2020-09-15 08:10:08 -07:00
FreeScout
b66a0a9b12 customer.created hook 2020-09-15 08:04:04 -07:00
FreeScout
fb7733f2e1 customer.created hook 2020-09-15 08:01:30 -07:00
FreeScout
d4fe8943a4 conversation.moved event 2020-09-15 07:48:13 -07:00
FreeScout
c3d6e845d0 conversation.deleted hook 2020-09-15 05:07:35 -07:00
Franklin
ba7b0843df Convert subject to UTF-8 encoding 2020-09-14 22:38:54 +02:00
FreeScout
5bbd6d474a Added .panel-padded class 2020-09-14 05:07:26 -07:00
FreeScout
e3d1d2760a settings.after_save hook 2020-09-14 05:06:13 -07:00
FreeScout
4489e171db fs_select2_config in main.js 2020-09-14 01:49:49 -07:00
FreeScout
9c32bf2328 Fix error on PostgreSQL in RealtimeMailboxNewThread - closes #731 2020-09-12 09:31:38 -07:00
FreeScout
1c687f84b7 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2020-09-11 04:42:33 -07:00
FreeScout
a574687253 v1.5.14 2020-09-11 04:41:49 -07:00
FreeScout
b7464a3742 Update readme 2020-09-11 04:35:02 -07:00
FreeScout
f319d2f498 Show address and country in customer profile 2020-09-11 04:11:29 -07:00
FreeScout
61bb42ae3a When creating a phone conversations try to find customer by phone number 2020-09-11 00:54:58 -07:00
FreeScout
e5bd0dfe49 Added function to save customer photo 2020-09-10 08:06:02 -07:00
FreeScout
f30adeb613 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2020-09-08 23:07:34 -07:00
FreeScout
ca173694f8 v1.5.13 2020-09-08 23:06:48 -07:00
FreeScout
9b04008545 Improve repeated auto reply detection - closes #688 2020-09-07 01:47:28 -07:00
FreeScout
83ca847312 auth.password_reset_available hook 2020-09-07 01:16:28 -07:00
FreeScout
19dcb55980 Add Modules folder to permissions 2020-09-06 06:42:19 -07:00
FreeScout
f1f1e19670 Update de tanslation 2020-09-06 06:39:16 -07:00
FreeScout
1bb23e2198 Search on Users page 2020-08-25 06:34:05 -07:00
FreeScout
adfd7baa46 Show emails on Users pages 2020-08-25 06:20:20 -07:00
FreeScout
c0b33e1ea5 Fix G Suite send error: Expected response code 250 but got an empty response 2020-08-25 03:42:06 -07:00
FreeScout
066bb84e5c Show Message-ID in email logs 2020-08-25 02:09:25 -07:00
FreeScout
e230ee77fd Improve rescaling images in threads 2020-08-24 23:41:06 -07:00
FreeScout
e113373eb6 Merge pull request #712 from fvdm/patch-2
Rescale large images for readability
2020-08-25 09:37:44 +03:00
Franklin
dd602cd3b6 Rescale large images for readability 2020-08-24 20:28:09 +02:00
FreeScout
ce029693cf Add vendor files to dist 2020-08-24 02:56:40 -07:00
FreeScout
497af1fe00 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2020-08-24 02:54:46 -07:00
FreeScout
b5e5bc0b01 Merge branch 'master' of github.com:freescout-helpdesk/freescout 2020-08-24 02:53:56 -07:00
FreeScout
051b1875e7 v1.5.12 2020-08-24 02:53:21 -07:00
FreeScout
a673f4a7f2 Merge pull request #708 from Zen-glen/master
Customer factory now generates json for 'phones' field.
2020-08-24 12:36:12 +03:00
FreeScout
bd1804589a Custom middleware hook 2020-08-23 10:22:33 -07:00
FreeScout
26409139a0 User profile menu hook 2020-08-23 10:22:00 -07:00
Glen Clark
62e44488a9 Customer factory now generates json for 'phones' field. 2020-08-23 10:57:22 +01:00
FreeScout
5def82024d Add Validate event to SessionGuard 2020-08-22 23:07:25 -07:00
FreeScout
cdb6b6d8f8 Allow to add macroses dynamically to Eloquent Models 2020-08-22 23:07:02 -07:00
FreeScout
4705c3ec8c Fix showing additional CCs in conversations - closes #703 2020-08-22 01:14:13 -07:00
FreeScout
ded547a296 Update PL translatons 2020-08-18 02:28:28 -07:00
FreeScout
c61655bb08 Merge pull request #699 from e1mo/master
Check if ORIG_SCRIPT_NAME exists before using it
2020-08-18 11:25:37 +03:00
FreeScout
eb25eeef76 Upate pr-BR translation 2020-08-18 01:11:44 -07:00
FreeScout
1de015a3ab Restore NL translation 2020-08-17 05:13:50 -07:00
e1mo
b83dbbe5d4 Check if ORIG_SCRIPT_NAME exists before using it 2020-08-16 20:02:10 +02:00
FreeScout
ed82534030 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2020-07-26 04:16:38 -07:00
FreeScout
94481421ac v1.5.11 2020-07-26 04:15:25 -07:00
FreeScout
3463563e42 Fix prev and next buttons in Mine folder 2020-07-26 01:26:49 -07:00
FreeScout
b6fac871e0 Main menu hooks 2020-07-26 00:38:44 -07:00
FreeScout
6221193d8c Do not show loader when automatically updating conversations - closes #643 2020-07-24 01:26:30 -07:00
FreeScout
83f14010b1 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2020-07-23 00:31:01 -07:00
FreeScout
adb6f8327d v1.5.10 2020-07-23 00:29:51 -07:00
FreeScout
bb7ede7998 Update user permissions 2020-07-23 00:28:54 -07:00
FreeScout
b6103a61cd folder.count filter 2020-07-20 00:57:51 -07:00
FreeScout
8ac4b2c7dd Fix folder conversations number in title 2020-07-20 00:57:40 -07:00
FreeScout
ed15907e69 Fix fetching emails in PostgreSQL 2020-07-19 03:21:42 -07:00
FreeScout
022f358e16 Sort mailboxes by name 2020-07-19 03:20:16 -07:00
FreeScout
661ce426dd Fix installation wizard with PostgreSQL - closes #654 2020-07-19 02:27:31 -07:00
FreeScout
068b89fe72 Reduce scrollable dropdown height 2020-07-19 00:54:03 -07:00
FreeScout
0924ebe33d Add missing time zones 2020-07-18 07:27:10 -07:00
FreeScout
97b7429da5 Alphabetically Sort Mailbox List on Dashboard - closes #660 2020-07-18 07:01:42 -07:00
FreeScout
cc1c392418 Fix flatpickr pt-pt.js error 2020-07-18 06:56:43 -07:00
FreeScout
1e8ce0bebf modules.show_license hook 2020-07-18 01:19:28 -07:00
FreeScout
badcdf78c1 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2020-07-17 07:20:59 -07:00
FreeScout
026bf60751 v1.5.9 2020-07-17 07:19:18 -07:00
FreeScout
6aee85a591 Add Conversation History option to conversations 2020-07-15 02:09:39 -07:00
FreeScout
f8e150c65d Merge branch 'master' of github.com:freescout-helpdesk/freescout 2020-07-15 01:39:04 -07:00
FreeScout
694d0ac27c Merge pull request #672 from Dirk-Kokx/master
Add Conversation History option to conversations
2020-07-15 11:38:46 +03:00
Dirk Kokx
2ad683f7dd Conversation:
changed database type and handling of the settings
2020-07-15 09:56:55 +02:00
Dirk Kokx
da367ee93f Conversation:
moved settings button to the end of the dropdown
2020-07-15 09:56:12 +02:00
Dirk Kokx
190d2304bc ...and now has a settings button to toggle the settings modal 2020-07-15 08:10:39 +02:00
Dirk Kokx
bdc39b25b7 Added conversation settings
For now this only includes history settings (an override to the global setting per conversation)
2020-07-14 22:11:36 +02:00
FreeScout
9b17434c64 Helper functions 2020-07-03 02:26:23 -07:00
FreeScout
65426df448 Merge pull request #650 from Matslom/fix-648
Fixes 648 "Hide from Assign list" in Bulk Actions
2020-07-01 06:59:14 +05:45
Mateusz Słomiński
05977c7792 Fixes 648 "Hide from Assign list" in Bulk Actions 2020-06-30 20:53:04 +02:00
FreeScout
804e92e9b7 Merge branch 'master' of github.com:freescout-helpdesk/freescout 2020-06-17 02:34:19 -07:00
FreeScout
02420b5e94 Merge pull request #639 from cloudron-io/brand-icon
Replace brand logo with svg version to support hiDPI screens
2020-06-17 15:17:28 +05:45
Johannes Zellner
b3f7016515 Replace brand logo with svg version to support hiDPI screens 2020-06-17 10:51:27 +02:00
FreeScout
96c7166a26 Remove delay when sending notifications to users 2020-06-16 21:25:26 -07:00
FreeScout
4262a1b639 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2020-06-15 21:27:57 -07:00
FreeScout
ad9316377d 1.5.8 2020-06-15 21:26:52 -07:00
FreeScout
c282f9c2d4 Perform second mail send attempt after 5 min 2020-06-15 19:26:08 -07:00
FreeScout
52a8ce60d9 Vertical scroll for large assignees list - closes #620 2020-06-15 19:25:08 -07:00
FreeScout
517a87438e Update German translations 2020-06-15 18:52:49 -07:00
FreeScout
cf4d03b772 Upadte NL translations 2020-06-15 18:51:16 -07:00
FreeScout
8877be24b5 Add delay when sending notifications to users to avoid blocking by G Suite Relay 2020-06-11 19:00:21 -07:00
FreeScout
1bbe9e19de Extra class for empty partial 2020-06-03 03:22:46 -07:00
FreeScout
6728f7c796 Add .glyphicon-spin class 2020-06-01 22:31:36 -07:00
FreeScout
5488f4ba3d Add .content-full class 2020-06-01 19:16:25 -07:00
FreeScout
026b4aa7cf Dont send auto reply for spam emails - closes #628 2020-06-01 19:15:42 -07:00
FreeScout
ab21227bee Convert menu.append filter into action 2020-05-28 23:17:50 -07:00
FreeScout
de0afbabde Update readme 2020-05-28 03:38:41 -07:00
FreeScout
ef033e477c Add vendor files to dist 2020-05-28 03:35:33 -07:00
FreeScout
35b2cbc490 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2020-05-28 03:28:03 -07:00
FreeScout
ecc031422e v1.5.7 2020-05-28 03:27:12 -07:00
FreeScout
7451223e09 Sort threads in emails to customers by thread ID instead of created_at - closes #532 2020-05-28 02:21:14 -07:00
FreeScout
9a1ae415ce Reply from customer to deleted conversation should undelete it - closes #585 2020-05-27 22:47:26 -07:00
FreeScout
e09fe00563 Do not send autoreplies to own mailboxes 2020-05-27 22:13:17 -07:00
FreeScout
0d8ae55422 Remove slug column from mailboxes table 2020-05-27 21:19:44 -07:00
FreeScout
ee508a7927 Let getInProtocolName work when protocol not set 2020-05-27 20:57:24 -07:00
FreeScout
ec8b282767 Deactivate license from Modules page 2020-05-27 03:05:06 -07:00
FreeScout
face17249d PHP 7.4 support 2020-05-27 02:59:01 -07:00
FreeScout
ce182c9fa5 Fix Select all in next page - closes #618 2020-05-27 01:36:52 -07:00
FreeScout
b25a59e748 PHP 7.4 support - closes #432 2020-05-26 03:45:33 -07:00
FreeScout
3b0c50329a Fix error when moving conversation to the mailbox where current assignee is not present 2020-05-26 03:36:23 -07:00
FreeScout
420245e66a Merge pull request #617 from springboardVR/feature/postgres
Postgres Support
2020-05-26 14:10:51 +05:45
Matthew Hall
dd9db08431 Postgres support 2020-05-20 09:00:21 -04:00
FreeScout
f8122d5fb3 Fix viewers icons on mobile 2020-05-16 21:24:22 -07:00
FreeScout
90975c31d2 Fix sidebar buttons width 2020-05-16 21:20:07 -07:00
FreeScout
8dd630b43b Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2020-05-16 03:45:30 -07:00
FreeScout
25a41858fd v1.5.6 2020-05-16 03:44:02 -07:00
FreeScout
d20986949b Updated translations: de, es, fr, pl 2020-05-16 03:42:17 -07:00
FreeScout
bc60281379 Allow to cancel queued job - closes #606, #611 2020-05-16 03:17:42 -07:00
FreeScout
84b3df80e2 View some attachments in browser instead of downloading - closes #609 2020-05-15 21:11:37 -07:00
FreeScout
8923315995 Show mailbox in search 2020-05-15 19:10:14 -07:00
FreeScout
528b4584ac Fix sorting in search - closes ##608 2020-05-14 04:44:27 -07:00
FreeScout
c14d2c3867 Show no subject for previous conversations without a subject 2020-05-14 01:58:00 -07:00
FreeScout
be3081de42 New reply separator 2020-05-14 01:54:24 -07:00
FreeScout
c46e0d9811 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2020-05-08 20:48:25 -07:00
FreeScout
f00df1c17a v1.5.5 2020-05-08 20:47:44 -07:00
FreeScout
684f8769a1 In customer profile show only conversation from mailboxes available for the user - closes #587 2020-05-08 20:46:34 -07:00
FreeScout
956888956e Preserve the thread id if any for reply copies 2020-05-08 19:27:36 -07:00
FreeScout
d8de1f86cf Merge branch 'master' of github.com:freescout-helpdesk/freescout 2020-05-08 19:17:58 -07:00
FreeScout
4be4b84622 Merge pull request #600 from cloudron-io/preserve-thread-id-for-reply-copies
Preserve the thread id if any for reply copies
2020-05-09 08:02:27 +05:45
FreeScout
ce1fbb5e05 Fix redirect to the folder after changing conversation status 2020-05-08 19:14:43 -07:00
Johannes Zellner
c29a7cd693 Preserve the thread id if any for reply copies 2020-05-02 14:05:29 +02:00
FreeScout
b69361abf2 Merge pull request #596 from fvdm/patch-1
Update NL translation
2020-05-02 17:37:29 +05:45
Franklin
e7e50bee35 Reordered keys NL translation 2020-04-30 12:08:23 +02:00
Franklin
2cef319a32 Update NL translation 2020-04-30 12:06:41 +02:00
Franklin
9df7c18181 Update NL translation 2020-04-30 11:49:21 +02:00
FreeScout
b4229070e6 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2020-04-26 03:11:24 -07:00
FreeScout
c11632f8ef 1.5.4 2020-04-26 03:10:30 -07:00
FreeScout
972cd5e645 Fix an array to string conversion error 2020-04-26 03:02:06 -07:00
FreeScout
87e606699d Improve saving reply emails to IMAP folder 2020-04-26 02:53:02 -07:00
FreeScout
5da4dee1cb Merge pull request #577 from cloudron-io/copy-reply-to-sent
Save reply email to mailbox
2020-04-26 15:00:42 +05:45
FreeScout
0d44809496 Merge branch 'master' of github.com:freescout-helpdesk/freescout 2020-04-25 03:54:33 -07:00
FreeScout
74e0108d76 Update readme 2020-04-25 03:54:19 -07:00
Johannes Zellner
243fe9c756 Save reply email to mailbox
If reply_folder is empty, no mails are stored on the IMAP server
automatically

Fixes #574
2020-04-16 15:32:25 +02:00
FreeScout
276dff3ae1 Merge pull request #576 from ChrisRAoW/master
Fix for reply on notification e-mail not assigned to original conversion
2020-04-13 07:45:00 +05:45
ChrisRAoW
e1ed469ec9 Fix for reply on notification e-mail not assigned to original conversion
Mac mail (at least) removes elements that are set to display none when you're replying. Because the {#FS:****#} was in an hidden div it was removed, and freescout was not able anymore to relate the reply to the original conversation. Simply removing the display none fixes that.
2020-04-12 20:44:50 +02:00
FreeScout
d60ac945fc Merge pull request #573 from cloudron-io/invite-subject
Use getCompanyName() wrapper for fallback value
2020-04-12 17:18:22 +05:45
Johannes Zellner
82d0f81498 Use getCompanyName() wrapper for fallback value
Fixes #570
2020-04-12 12:24:03 +02:00
FreeScout
f176c1f117 Fix Undefined variable folder in FetchEmails.php 2020-04-03 04:11:12 -07:00
FreeScout
341f4c006f Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2020-03-29 02:10:41 -07:00
FreeScout
a86721f992 v1.5.3 2020-03-29 02:09:57 -07:00
FreeScout
d50805da83 Add setting for conversation history in user notifications - closes #375 2020-03-28 22:07:54 -07:00
FreeScout
7532cec613 Merge pull request #548 from fvdm/feature-userhistory
Add setting for conversation history in user notifications
2020-03-29 10:30:38 +05:45
FreeScout
6ef4e44c88 Allow to exclude INBOX from IMAP folders 2020-03-28 21:33:24 -07:00
FreeScout
c147acedda Merge branch 'master' of github.com:freescout-helpdesk/freescout 2020-03-28 20:55:28 -07:00
FreeScout
b60c4e24e6 Merge pull request #547 from rosinghal/feature/imap-inbox-not-default
Feature/imap inbox not default
2020-03-29 09:40:03 +05:45
FreeScout
b4d8c82e04 Do not fetch from mailboxes with empty password 2020-03-28 20:44:51 -07:00
Franklin
4c197400da Whitespace fix 2020-03-24 14:23:23 +01:00
Franklin
b422c44603 Add config conv history in user notifications 2020-03-24 13:57:29 +01:00
Rohit Singhal
8fd7a5ad8f default folder to be INBOX 2020-03-20 12:20:30 +05:30
FreeScout
5cfcf77c32 Merge pull request #535 from fvdm/patch-1
Add option to include the last message in notifications
2020-03-16 09:36:18 +05:45
Franklin
e15a40f7d9 Roll back email_conv_history in user notifications 2020-03-14 10:49:16 +01:00
FreeScout
62905aa073 Merge pull request #538 from fvdm/patch-2
Add .editorconfig for consistent whitespace
2020-03-14 10:27:30 +05:45
Franklin
b98d3f7be2 Add .editorconfig for consistent whitespace 2020-03-13 17:21:42 +01:00
Franklin
1808a0473c Remove inconsistent whitespace 2020-03-13 17:15:57 +01:00
Franklin
1d8d609307 Fix ugly whitespace 2020-03-13 16:43:41 +01:00
Franklin
c8f9645eae Allow to include only the last message in user notifications 2020-03-13 16:40:19 +01:00
Franklin
0b83f75e14 Add option to include last message of conversation 2020-03-13 16:00:37 +01:00
Franklin
ee2199afce Allow to include only the last message 2020-03-13 15:57:00 +01:00
FreeScout
b7a3719815 Add vendor files to dist 2020-03-12 23:05:28 -07:00
FreeScout
2b7a5c31d4 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2020-03-12 23:03:14 -07:00
FreeScout
015c72292f v1.5.2 2020-03-12 23:02:29 -07:00
FreeScout
303619d060 jobs.send_reply_to_customer.send_previous_messages filter 2020-03-12 23:00:17 -07:00
FreeScout
219c8c0ccc Update Readme 2020-03-12 22:52:55 -07:00
FreeScout
d3158cc8a1 Fix The filename fallback must only contain ASCII characters - closes #533 2020-03-12 22:08:04 -07:00
FreeScout
d4c69004cc Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2020-03-12 01:57:14 -07:00
FreeScout
c0cd027904 v1.5.1 2020-03-12 01:56:38 -07:00
FreeScout
12675a1586 Option to send full conversation history to customers - closes #447 2020-03-12 01:48:54 -07:00
FreeScout
a51d89f559 Merge branch 'master' of github.com:freescout-helpdesk/freescout 2020-03-09 04:04:51 -07:00
FreeScout
2c4d41f8da Merge pull request #520 from cloudron-io/user-create-args
Add commandline arguments to freescout:create-user
2020-03-09 16:49:33 +05:45
FreeScout
6491fc3ddd Do not log encoding conversion errors 2020-03-09 03:59:35 -07:00
Rohit Singhal
a51189e6f6 Removing INBOX as default IMAP folder 2020-03-09 08:07:12 +05:30
FreeScout
3bdee559e6 Add vendor files to dist 2020-03-07 20:08:44 -08:00
FreeScout
de67e11704 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2020-03-07 20:07:04 -08:00
FreeScout
180380339d v1.5.0 2020-03-07 20:06:33 -08:00
FreeScout
1d7be34787 APP_DOWNLOAD_ATTACHMENTS_VIA 2020-03-07 20:01:57 -08:00
Johannes Zellner
d8e3c1a390 Add commandline arguments for feescout:create-user for automation 2020-03-07 17:14:53 -08:00
FreeScout
c4bf136fbd Check max PHP version in the installer 2020-03-07 03:31:47 -08:00
FreeScout
dc567ea94f Merge pull request #518 from fvdm/patch-1
Remove trailing n from relative date minutes for Dutch systems
2020-03-07 15:59:58 +05:45
FreeScout
b4133dbb7e Secure attachments - closes #308 2020-03-07 02:02:03 -08:00
Franklin
3c8f2855bf Fix relative date for Dutch translation 2020-03-06 13:06:41 +01:00
FreeScout
9583a2a9e9 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2020-03-05 02:55:59 -08:00
FreeScout
37646b4f63 v1.4.12 2020-03-05 02:55:24 -08:00
FreeScout
96adf59413 Properly decode sender name - closes #516 2020-03-05 02:46:36 -08:00
FreeScout
6fadc6f4f8 Merge branch 'master' of github.com:freescout-helpdesk/freescout 2020-03-05 02:22:41 -08:00
FreeScout
4a2be8898d Improve autoresponders detection - closes #513 2020-03-05 02:22:19 -08:00
FreeScout
8bfbc93cf8 Show mailbox email in the thread CC 2020-03-05 02:09:48 -08:00
FreeScout
cb139b11ca Show only native languages in the language dropdown 2020-03-05 01:44:27 -08:00
FreeScout
e1e3ccc599 Polish language 2020-03-05 01:39:39 -08:00
FreeScout
00e7d0793a Save mailbox email as customer 2020-03-05 01:30:54 -08:00
FreeScout
6a0e2528db Merge pull request #508 from carsso/master
Updating French translations
2020-03-03 13:33:02 +05:45
Germain Carré
c612810a2a Updating French translations 2020-02-27 23:33:41 +01:00
FreeScout
21243fd7a0 Allow to disable users to prevent them from logging in 2020-02-21 21:53:51 -08:00
FreeScout
ca1b010529 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2020-02-18 00:52:07 -08:00
FreeScout
a2d7297382 v1.4.11 2020-02-18 00:51:28 -08:00
FreeScout
6052103ebf conversation.create_form.subject_append hook 2020-02-18 00:49:01 -08:00
FreeScout
383cbe953f Update dutch translation 2020-02-17 19:01:35 -08:00
FreeScout
75d20f7e0f Allow recipient name in the To field - closes #305 2020-02-16 23:05:15 -08:00
FreeScout
9fe7644f0a Merge branch 'master' of github.com:freescout-helpdesk/freescout 2020-02-16 22:27:06 -08:00
FreeScout
863c41d550 Merge pull request #488 from Qrious/multi-email-input
Fix tokenization of multiple emails on copy+paste of multiple emails
2020-02-17 12:11:50 +05:45
FreeScout
5280f71e15 Escape user name in This reply will go to the customer message 2020-02-16 22:23:48 -08:00
FreeScout
df7b0772c8 Allow admins to mute notifications for events not related directly to him/her 2020-02-16 21:13:05 -08:00
Sander Klock
4ea63506a5 Fix tokenization of multiple emails on copy+paste of string containing multiple emails 2020-02-16 16:57:33 +01:00
FreeScout
fae1253df6 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2020-02-15 22:01:55 -08:00
FreeScout
d9e074762b v1.4.10 2020-02-15 22:01:09 -08:00
FreeScout
096cd4f9b9 Command to logout users: freescout:logout-users 2020-02-15 19:10:55 -08:00
FreeScout
39f6c8959e Swedish language 2020-02-15 02:27:08 -08:00
FreeScout
3824bcb359 Concatenate multiple plain text parts on fetching emails 2020-02-15 00:40:13 -08:00
FreeScout
3b56ce55ed Caret style in conversation bulk actions 2020-02-14 03:17:42 -08:00
FreeScout
31b8c24b4b Ignore git folder when importing translations 2020-02-10 21:58:35 -08:00
FreeScout
a0b97b51c9 Show (no subject) when viewing conversation without a subject 2020-02-09 21:18:11 -08:00
FreeScout
f45925d34d Add IfModule to attachment .htaccess 2020-02-09 21:05:55 -08:00
FreeScout
90a34c1295 Fix placeholders in French translations - closess #469 2020-02-09 02:35:02 -08:00
FreeScout
42717759b6 Spanish translation 2020-02-09 02:29:55 -08:00
FreeScout
695ca73817 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2020-02-09 00:12:04 -08:00
FreeScout
f21a95940e v1.4.9 2020-02-09 00:11:21 -08:00
FreeScout
7fe21927c9 Force downloading attachments on Apache 2020-02-08 03:19:12 -08:00
FreeScout
8338777318 Notice on placeholders in translations 2020-02-08 03:03:27 -08:00
FreeScout
e2c1b65e9e Focus search input on view - closes #470 2020-02-08 02:58:40 -08:00
FreeScout
e4f397d593 Show user vars in mailbox signature and hide in Auto Reply - closes #458 2020-02-08 02:38:21 -08:00
FreeScout
c0893aa500 Hide admins from Assign list - closes #456 2020-02-06 02:52:06 -08:00
FreeScout
726d78d4f8 Show Assignee in custom folders - closes #465 2020-02-04 23:08:10 -08:00
FreeScout
918e31483b Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2020-02-02 06:37:30 -08:00
FreeScout
54960a9a99 v1.4.8 2020-02-02 06:35:48 -08:00
FreeScout
71feae1ee1 Merge branch 'master' of github.com:freescout-helpdesk/freescout 2020-02-01 20:44:00 -08:00
FreeScout
317bf35c9c Merge pull request #459 from santibm/master
Feature request: adding images to mailbox signature #366 and showing remove button in main toolbar in desktop
2020-02-02 10:28:49 +05:45
FreeScout
73b8fa039f Fix utf subject inlowercase 2020-02-01 20:15:53 -08:00
SBM
8e1412c014 Showing remove button in main toolbar in desktop #454 2020-02-01 22:23:37 +01:00
SBM
8cc777013c Removing unused route for adding images to mailbox signature #366 2020-02-01 20:25:57 +01:00
SBM
750f98648d Changes for adding images to mailbox signature #366 2020-02-01 20:23:01 +01:00
SBM
995f36075a Feature request: adding images to mailbox signature #366 2020-02-01 10:24:34 +01:00
FreeScout
74a56142f6 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2020-01-28 22:42:29 -08:00
FreeScout
cd9fc3e5a3 v1.4.7 2020-01-28 22:41:58 -08:00
FreeScout
c8001a615e Return user in User::create function 2020-01-28 02:39:43 -08:00
FreeScout
7be3266543 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2020-01-25 05:35:32 -08:00
FreeScout
d0f013c536 1.4.6 2020-01-25 05:35:14 -08:00
FreeScout
d8f36cd716 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2020-01-25 05:30:36 -08:00
FreeScout
9718d42c13 v1.4.5 2020-01-25 05:30:09 -08:00
FreeScout
ed46dca3da bulk_actions.before_delete hook 2020-01-25 05:07:53 -08:00
FreeScout
07f40b5c15 Fix headers decoding issue 2020-01-25 04:30:15 -08:00
FreeScout
e004968328 Fix detecting bounced messages 2020-01-25 01:28:15 -08:00
FreeScout
4a71d6fa73 Process github api errors on System page - closes #449 2020-01-24 23:34:05 -08:00
FreeScout
b29db2c76c Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2020-01-24 05:47:48 -08:00
FreeScout
a817e001db v1.4.4 2020-01-24 05:46:54 -08:00
FreeScout
35341a9caf Update French translations 2020-01-24 05:33:50 -08:00
FreeScout
7e469d6bea Do no escape person in getActionText 2020-01-24 00:04:42 -08:00
FreeScout
c1a5999471 Escape signature variables 2020-01-22 23:09:39 -08:00
FreeScout
9a193be6f3 Notice for Background Jobs in Status 2020-01-21 01:43:51 -08:00
FreeScout
9464f46bf6 Fix publishing modules translations 2020-01-21 00:36:49 -08:00
FreeScout
f24a12ef45 Merge pull request #440 from TimoStramann/patch-2
Update de.json
2020-01-21 11:00:22 +03:00
Timo Stramann
f36252ac87 Update de.json 2020-01-20 22:51:54 +01:00
FreeScout
2653bf49d0 Merge pull request #438 from santibm/master
Adding new filters to allow hooking menu items from third party modules
2020-01-20 15:15:45 +03:00
SBM
781b51ea88 Fix on adding new filters to allow hooking menu items from third party modules 2020-01-20 13:14:48 +01:00
SBM
36147e7b2f Adding new filters to allow hooking menu items from thrid party modules 2020-01-20 12:03:02 +01:00
FreeScout
8842a097ab Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2020-01-19 08:50:08 -08:00
FreeScout
2a1445c060 v1.4.3 2020-01-19 08:49:33 -08:00
FreeScout
bd24a34f8f Merge branch 'master' of github.com:freescout-helpdesk/freescout 2020-01-19 08:44:58 -08:00
FreeScout
fcee9b2003 Add rememberCacheDriver to rememberable entities 2020-01-19 01:09:42 -08:00
FreeScout
923a2db2d0 Move calendar to the partial 2020-01-19 00:59:14 -08:00
FreeScout
59c2b6fa0f Merge pull request #434 from TimoStramann/master
Added German translation
2020-01-18 14:06:07 +03:00
Timo Stramann
9984c4453f Added German translation
Most strings translated into German
2020-01-18 11:55:15 +01:00
FreeScout
b439c8db3b Update readme 2020-01-17 20:55:15 -08:00
FreeScout
cb828d577e Fix Undefined variable cipher - closes #432 2020-01-17 11:02:06 -08:00
FreeScout
4b48841caa Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2020-01-17 04:38:40 -08:00
FreeScout
adf422687a v1.4.2 2020-01-17 04:38:02 -08:00
FreeScout
3a99faa85c Fix In-Reply-To header in replies to customer - closes #428 2020-01-16 23:30:45 -08:00
FreeScout
e245659e5f Sort users alphabetically - closes #427 2020-01-16 22:41:48 -08:00
FreeScout
17d85512a2 Sort users alphabetically - closes #427 2020-01-16 00:08:45 -08:00
FreeScout
727d967823 German language 2020-01-15 23:47:20 -08:00
FreeScout
54a51d9d82 Merge branch 'master' of github.com:freescout-helpdesk/freescout 2020-01-15 23:35:22 -08:00
FreeScout
5a0289a4b6 Merge pull request #426 from santibm/master
Spaces in some conversation titles are replaced by underscores #420
2020-01-16 10:34:38 +03:00
FreeScout
790181a720 Merge pull request #425 from TimoStramann/patch-1
Adding German translation
2020-01-16 09:33:43 +03:00
SBM
a440b8d3bc Spaces in some conversation titles are replaced by underscores #420 2020-01-15 23:15:31 +01:00
Timo Stramann
e64025fb30 Adding German translation
German translation of all currently available strings
2020-01-15 22:39:37 +01:00
FreeScout
904d1650cd thread.opened hook 2020-01-14 23:54:37 -08:00
FreeScout
4765d96cec Year in footer 2020-01-14 23:17:50 -08:00
FreeScout
6a85c9588f Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2020-01-14 00:52:58 -08:00
FreeScout
7a6157fd12 v1.4.1 2020-01-14 00:52:22 -08:00
FreeScout
f8cf6f9fb6 Save to the log only first failed send attempt - closes #413 2020-01-14 00:48:02 -08:00
FreeScout
2cd39eb5c9 findNonDeleted user function 2020-01-13 09:01:38 -08:00
FreeScout
f823d7527d Set has_attachments when replying 2020-01-13 00:59:19 -08:00
FreeScout
70732b7f94 Update readme 2020-01-12 04:01:32 -08:00
FreeScout
6bf9858c94 mailbox.before_delete hook 2020-01-10 23:41:40 -08:00
FreeScout
f46c9847ce addFloatingFlash function 2020-01-10 23:41:31 -08:00
FreeScout
7a6480a5a1 addFlashMessage helper function 2020-01-10 23:05:55 -08:00
FreeScout
6e1b95ff59 UserDeleted event params 2020-01-10 21:27:53 -08:00
FreeScout
faf39b0e9c deleteToFolder function for conversation 2020-01-10 21:15:50 -08:00
FreeScout
bae93001f0 Update readme 2020-01-10 21:15:04 -08:00
FreeScout
a55985b415 createUserThread function 2020-01-09 06:04:12 -08:00
FreeScout
794242f42f thread.meta hook 2020-01-09 04:24:17 -08:00
FreeScout
16846cfa26 Thread meta link style 2020-01-09 04:24:03 -08:00
FreeScout
a59e55642b Properly choose new thread in Subscribe 2020-01-08 09:48:02 -08:00
FreeScout
78b074499b Check all threads 2020-01-08 06:13:52 -08:00
FreeScout
83851ba366 Do not allow users delete themselve 2020-01-07 22:47:58 -08:00
FreeScout
740f684324 Logout deleted users 2020-01-07 22:44:29 -08:00
FreeScout
f5240ed605 Fix view.blade.php html 2020-01-07 10:44:28 -08:00
FreeScout
1d32c0f2cd Show mailbox fetching status 2020-01-07 05:19:59 -08:00
FreeScout
a0d251eb9a Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2020-01-07 02:12:00 -08:00
FreeScout
6ecda5b99b v1.4.0 2020-01-07 02:11:22 -08:00
FreeScout
61d4c735b3 Conversations I'm following search bookmark - closes #350 2020-01-07 02:08:06 -08:00
FreeScout
d083f4bdf9 Followed conversations search bookmark 2020-01-07 01:11:57 -08:00
FreeScout
3175fd6518 View followed conversations in the search - closes #406 2020-01-07 00:51:08 -08:00
FreeScout
9586c67d53 Send emails separately to each recipient checkbox on create 2020-01-07 00:35:57 -08:00
FreeScout
8db93a4ee4 conversation.created hook 2020-01-06 02:00:08 -08:00
FreeScout
585f35546f changeUser function 2020-01-04 01:00:43 -08:00
FreeScout
4eaadab2bc thread.created hook 2020-01-04 00:38:47 -08:00
FreeScout
7e3781a662 Automatically cast Thread meta to array 2020-01-03 23:37:47 -08:00
FreeScout
286ac988c7 Empty Trash button - closes #263 2020-01-03 02:16:33 -08:00
FreeScout
deaa8a47be Fix uploaded file position in reply form 2020-01-03 01:08:44 -08:00
FreeScout
b1d0ae9fee Delete single conversation forever 2020-01-03 01:03:36 -08:00
FreeScout
d69a5bfcda Update fr translation 2020-01-02 23:45:51 -08:00
FreeScout
b63dfb136f Remove dot before @ in emails 2020-01-02 23:42:49 -08:00
FreeScout
aa58d5fe75 Merge branch 'master' of github.com:freescout-helpdesk/freescout 2020-01-02 23:29:28 -08:00
FreeScout
aab1c3e85b changeStatus function for conversation 2020-01-02 23:22:39 -08:00
FreeScout
cbd00ca192 Update forward and move icons 2020-01-02 05:55:26 -08:00
FreeScout
3e4392d4ad USER_UNASSIGNED const 2019-12-31 04:19:58 -08:00
FreeScout
f5d85b8594 Style for reply label 2019-12-31 04:19:41 -08:00
FreeScout
b1966a2a85 Pass trigger to modals 2019-12-31 04:19:21 -08:00
FreeScout
0516ebaa57 Merge pull request #400 from wevothere120/master
Fix #374 by ignoring > in local part of mail regex
2019-12-30 21:12:47 +03:00
wevothere120
679fa7546c OR operator wasn't actually necessary in a36228807c 2019-12-30 18:57:14 +01:00
wevothere120
e76891b280 fixes freescout-helpdesk/freescout#374
Although in some cases it is allowed to use ">" in local part of email address, it is not very common, so it shouldn't break anything. Tested on bunch of threads and found no issues
2019-12-30 18:56:53 +01:00
FreeScout
4d5da851ab Default label color 2019-12-29 23:48:38 -08:00
FreeScout
d8c848cfae Update readme 2019-12-28 05:44:21 -08:00
FreeScout
2df4c024f4 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2019-12-28 04:00:17 -08:00
FreeScout
24d4971abe v1.3.19 2019-12-28 03:59:25 -08:00
FreeScout
cc32cc8257 .col classes 2019-12-27 23:38:32 -08:00
FreeScout
bd52e9c72d conv-sidebar-block on mobile 2019-12-27 23:08:27 -08:00
FreeScout
40b62be293 conversation.follow JS hook 2019-12-27 22:31:40 -08:00
FreeScout
3db52a6c41 isAjaxSuccess function 2019-12-27 21:24:48 -08:00
FreeScout
43f7ebb225 Sidebar block style 2019-12-27 01:04:18 -08:00
FreeScout
bd070a8af9 Sort mailbox users by full name by defaul 2019-12-27 01:03:54 -08:00
FreeScout
8e379b14f1 Sort mailbox users by full name by default 2019-12-26 04:38:04 -08:00
FreeScout
2d0cb64ec3 Rename conversation.after_customer hook into conversation.after_prev_convs 2019-12-26 04:12:23 -08:00
FreeScout
6291f48263 Conversation actions bottons hooks 2019-12-26 00:57:36 -08:00
FreeScout
f103bda6fe Icons for conversation actions dropdown 2019-12-26 00:53:46 -08:00
FreeScout
7ea54b6f03 Following conversations 2019-12-25 05:17:17 -08:00
FreeScout
f4d04f6691 Fix starring conversations 2019-12-23 05:54:36 -08:00
FreeScout
a2efc65226 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2019-12-22 01:38:20 -08:00
FreeScout
c5117c7898 v.1.3.18 2019-12-22 01:37:34 -08:00
FreeScout
86761c405c Fix Row size too large error 2019-12-22 01:36:45 -08:00
FreeScout
4fc419ba74 Maximum password length notice 2019-12-22 01:21:21 -08:00
FreeScout
8686012302 Fix Row size too large error 2019-12-22 01:11:21 -08:00
FreeScout
90f3f4b56f Update readme 2019-12-21 08:50:18 -08:00
FreeScout
19edef364f Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2019-12-21 05:02:02 -08:00
FreeScout
ce307ddc45 1.3.17 2019-12-21 05:01:24 -08:00
FreeScout
948de91102 Update layout translation strings 2019-12-21 02:19:24 -08:00
FreeScout
ab751c3b27 Update ru translation 2019-12-21 02:19:12 -08:00
FreeScout
d01df11a21 Folder hooks 2019-12-20 01:41:06 -08:00
FreeScout
0e4c9efdbe Limit folder name width 2019-12-19 23:33:20 -08:00
FreeScout
80438e991c Folder hooks 2019-12-19 23:06:34 -08:00
FreeScout
d0e224dbe1 requiredModules parameter for modules 2019-12-19 01:18:52 -08:00
FreeScout
53138e405e Fix Row size too large error - closes #393 2019-12-18 23:02:26 -08:00
FreeScout
a3a1e0d621 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2019-12-18 09:58:42 -08:00
FreeScout
92efaab611 v1.3.16 2019-12-18 09:58:13 -08:00
FreeScout
a87156690b Update nl translation 2019-12-18 05:02:11 -08:00
FreeScout
fa01192bd3 layout hooks 2019-12-18 04:58:54 -08:00
FreeScout
0c2d9900f3 Upload file helper functions 2019-12-18 04:58:41 -08:00
FreeScout
ebd34d0d0a Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2019-12-16 23:44:21 -08:00
FreeScout
f3ea9bcb47 v1.3.15 2019-12-16 23:43:04 -08:00
FreeScout
33c5f74bf3 Do not allow to send empty reply 2019-12-16 23:40:30 -08:00
FreeScout
babb1f3956 isNewConversation js function 2019-12-16 10:06:20 -08:00
FreeScout
c0b48208e6 conversation.create_form.after_subject hook 2019-12-16 10:05:48 -08:00
FreeScout
b00b830f21 mailboxes.menu_current_route filter 2019-12-16 00:44:12 -08:00
FreeScout
a24c56b527 Fix customers ajax search pagination 2019-12-16 00:43:11 -08:00
FreeScout
bd1f4b06bc Label styles 2019-12-13 21:52:40 -08:00
FreeScout
c3cbeebf91 Add identifier to queue:work command to void conflicts with other FreeScout instances on the same server - closes #387 2019-12-13 21:52:23 -08:00
FreeScout
bba3f071ee Text Before The Reply option for the mailbox 2019-12-11 00:37:05 -08:00
FreeScout
eaf5bb245b Text Before The Reply option for the mailbox 2019-12-10 02:04:27 -08:00
FreeScout
52bc01dede Create customer from Change customer dialog - closes #49 2019-12-10 02:00:12 -08:00
FreeScout
1b3020d99a Text Before The Reply option for the mailbox - closes #349 2019-12-10 00:21:01 -08:00
FreeScout
046f0ce79d Remove user variables from the mailbox signature - closes #383 2019-12-09 23:12:58 -08:00
FreeScout
79c40bc505 Fix starred conversations counter 2019-12-09 22:51:49 -08:00
FreeScout
c822777ed3 Add vendor files to dist 2019-12-09 05:50:48 -08:00
FreeScout
29ebb5ad72 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2019-12-09 05:49:42 -08:00
FreeScout
4cdf9d9b63 v1.3.14 2019-12-09 05:48:23 -08:00
FreeScout
120f81e3f5 Fetch all bodies of emails with multiple HTML parts - closes #381 2019-12-09 05:31:54 -08:00
FreeScout
0c70621935 Remove trailing dots from emails - closes #379 2019-12-09 02:05:55 -08:00
FreeScout
9efa70a0ee Set connection timeout when downloading modules 2019-12-08 11:17:03 -08:00
FreeScout
b5dd14758b Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2019-12-08 09:01:36 -08:00
FreeScout
1cd8881088 v1.3.13 2019-12-08 09:00:46 -08:00
FreeScout
e0654d8e08 ru language 2019-12-08 09:00:34 -08:00
FreeScout
b30ffc19cd Remove modals from DOM on hide 2019-12-06 22:06:19 -08:00
FreeScout
3f0b64775f Dashboard dates size 2019-12-06 04:12:50 -08:00
FreeScout
02bfc295e6 Do not add module translation strings existing in the app 2019-12-06 02:56:51 -08:00
FreeScout
fcf957db9c Update readme 2019-12-06 02:02:42 -08:00
FreeScout
fe62c7fcd4 Add nl language 2019-12-06 02:01:53 -08:00
FreeScout
a15e87f23c Add ru language 2019-12-06 01:57:42 -08:00
FreeScout
4375c1ecd3 Allow translation strings in different case 2019-12-06 01:44:12 -08:00
FreeScout
d8d8ea3bd0 Revise translation strings 2019-12-06 01:28:22 -08:00
FreeScout
613c888d47 Fix counting changed translations 2019-12-06 01:11:16 -08:00
FreeScout
4c665f3749 Fix counting translations 2019-12-06 00:45:40 -08:00
FreeScout
307fe595f0 Fix importing jsons of locales with dashes 2019-12-05 23:17:12 -08:00
FreeScout
341d4ebf24 Fix importing translations 2019-12-05 23:06:35 -08:00
FreeScout
3adde31a47 Allow translation strings in different case 2019-12-05 10:00:44 -08:00
FreeScout
6acd19977c Revise translation strings 2019-12-05 09:59:46 -08:00
FreeScout
df9bddb85f Fix js messages translation - closes #363 2019-12-05 01:03:41 -08:00
FreeScout
3d6928fcdc Fix last reply date in conversation list 2019-12-04 23:29:43 -08:00
FreeScout
f242090f8d When importing translations remove nonexisting _app translations 2019-12-04 01:13:01 -08:00
FreeScout
dd071d4926 JS filters and scrolling to reply area 2019-12-03 01:09:33 -08:00
FreeScout
e38ccdee96 Sortable panel 2019-12-02 06:25:01 -08:00
FreeScout
7da26f92bf Catch exception when sending translation 2019-11-26 22:43:14 -08:00
FreeScout
14c04c8ce1 Increment retry interval in case SMTP server is not available 2019-11-26 21:49:16 -08:00
FreeScout
5e928270bb Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2019-11-25 09:24:25 -08:00
FreeScout
9661a938c5 v1.3.12 2019-11-25 09:23:49 -08:00
FreeScout
6ca806f733 Update sidebar folders in real-time 2019-11-25 09:20:23 -08:00
FreeScout
ebb8a1c60f Auto refresh conversations - closes #285 2019-11-25 06:19:36 -08:00
FreeScout
01d8d8b34d Update sidebar folders in real-time 2019-11-25 02:48:23 -08:00
FreeScout
17f280708e Dinamically load new messages, assignee and status when viewing the conversation 2019-11-25 01:23:16 -08:00
FreeScout
3a0e3b780a Fix styles when conversation top block is visible 2019-11-24 00:13:36 -08:00
FreeScout
3e1ca23348 Fix eventy null argument issue 2019-11-23 23:08:13 -08:00
FreeScout
22317cd092 Move conversation by entering the traget mailbox email - closes #367 2019-11-23 22:46:13 -08:00
FreeScout
9410d282cc When discarding a new conversation being created from thread return to original conversation 2019-11-23 03:13:43 -08:00
FreeScout
3e732ddbe6 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2019-11-17 05:20:02 -08:00
FreeScout
e4a7c2713f v1.3.11 2019-11-17 05:19:20 -08:00
FreeScout
32e40ed930 Fallback to additional freescout API url if can not connect to the main 2019-11-17 05:18:06 -08:00
FreeScout
1406b3566a conversations_table.after_subject hook 2019-11-17 03:10:08 -08:00
FreeScout
7022a5a992 conversation.view.start hook 2019-11-17 03:09:52 -08:00
FreeScout
ef98cec45a JS hooks 2019-11-17 01:10:16 -08:00
FreeScout
c0477f52f5 Take default settings values into account when saving options 2019-11-16 01:03:34 -08:00
FreeScout
111a0dcdca Pass prev_user_id to conversation.user_changed hook 2019-11-16 01:02:52 -08:00
FreeScout
0113623294 Rename conversation.status_changed_by_user hook into conversation.status_changed 2019-11-15 00:41:47 -08:00
FreeScout
c73857425a Rename conversation.status_changed_by_user hook into conversation.status_changed 2019-11-15 00:40:38 -08:00
FreeScout
a53374f94f conversation.status_changed hook 2019-11-15 00:32:24 -08:00
FreeScout
d0f771a78e Update readme features 2019-11-14 08:46:03 -08:00
FreeScout
0682433113 Send only support agent reply without full conversation history in emails to customers - closes #365 2019-11-14 00:17:50 -08:00
FreeScout
9f84983680 Update sidebar hamburger style 2019-11-13 23:40:07 -08:00
FreeScout
025b1e4a61 Do not show deleted users in the users dropdown - closes #364 2019-11-13 23:36:28 -08:00
FreeScout
5fbcf19389 Log fetch exceptions to activity log 2019-11-13 23:30:20 -08:00
FreeScout
0b4f4f915a Update mobile hamburger style 2019-11-13 06:18:29 -08:00
FreeScout
9c7cce6d07 Do not throw exception when parsing date on fetching 2019-11-13 05:29:02 -08:00
FreeScout
fd3ae64d9d Fix Conversion from cp-850 to UTF-8 is not allowed - closes #360 2019-11-13 04:41:17 -08:00
FreeScout
ece4e9f3e1 Fix checking module symlnk 2019-11-09 01:51:46 -08:00
FreeScout
fda982c7d1 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2019-11-08 23:23:22 -08:00
FreeScout
a4acf8caa7 v1.3.10 2019-11-08 23:22:50 -08:00
FreeScout
da08b93e25 Update readme 2019-11-08 23:17:58 -08:00
FreeScout
1f8229ca5c Mobile app features 2019-11-08 23:08:51 -08:00
FreeScout
4afdbb94cf Mobile app features 2019-11-07 00:45:01 -08:00
FreeScout
d591ec05c0 Switch helpdesk URL 2019-11-02 22:38:41 -07:00
FreeScout
9c971a0387 On update remove _tmp folders 2019-11-01 01:22:57 -07:00
FreeScout
e78d935a54 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2019-11-01 00:35:04 -07:00
FreeScout
1304f59bef v1.3.9 2019-11-01 00:34:09 -07:00
FreeScout
7a1aa3fb2a Allow to deactivate module license 2019-11-01 00:27:31 -07:00
FreeScout
712fd47880 Prevent public folder issue when activating modules 2019-10-31 03:43:21 -07:00
FreeScout
802c5cf20d Fix Message body is empty error - closes #320 2019-10-31 03:12:05 -07:00
FreeScout
8d2036636f Fix Wrong charset conversion from 1252 - closes #351 2019-10-31 01:18:36 -07:00
FreeScout
b0923098e4 Do not kill background workers of other laravel projects - closes #340 2019-10-31 00:50:51 -07:00
FreeScout
9673871c48 Checkout files in update.sh 2019-10-14 00:18:59 -07:00
FreeScout
d08d2ab786 Fix job_title and phone case in vars template 2019-10-14 00:08:57 -07:00
FreeScout
47608f26d5 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2019-10-14 00:01:27 -07:00
FreeScout
6dee385660 v1.3.8 2019-10-14 00:00:37 -07:00
FreeScout
ec7d82d663 Improve user photoUrl variable in mailbox signature 2019-10-13 23:59:00 -07:00
FreeScout
567b47f793 Fix customer avatar path in subdirectory 2019-10-13 23:57:32 -07:00
FreeScout
f2603c030d Merge pull request #348 from frommMoritz/enhancement/more-fields-in-signature
Photo URL as Variable in signature
2019-10-14 09:44:54 +03:00
Moritz Fromm
2d24ea0e43 Add photoUrl variable as signature template
Close #281
2019-10-11 23:10:41 +02:00
Moritz Fromm
7f70f51c34 Return absolute URL from user->getPhotoUrl()
Ref #281
2019-10-11 23:04:18 +02:00
FreeScout
a5c3424d17 Merge pull request #347 from frommMoritz/enhancement/auto-detect-links
Detect links inside the E-Mail and convert them to html links
2019-10-11 09:03:21 +03:00
FreeScout
9cb3d2fde6 Merge pull request #346 from frommMoritz/enhancement/no-previous-spam
Do not show spam mails in previous conversations
2019-10-11 08:58:39 +03:00
FreeScout
8526ba4d04 Improve new variables in mailbox signature 2019-10-10 22:57:22 -07:00
FreeScout
ffcd58ac46 Merge pull request #345 from frommMoritz/enhancement/more-fields-in-signature
More fields in signature
2019-10-11 08:19:16 +03:00
Moritz Fromm
1d02f1362c Detect links inside the E-Mail and convert them to html links
The code used was released under the public domain by @jasny
https://gist.github.com/jasny/2000705

Close #341
2019-10-11 00:29:35 +02:00
Moritz Fromm
278a636398 Do not show spam mails in previous conversations
Close #338
2019-10-10 21:47:12 +02:00
Moritz Fromm
831ca7c3cb Add new variables to composing ui, including translations
Close #281
2019-10-10 20:50:27 +02:00
Moritz Fromm
5604cf1356 Add more fields in signature
user.jobTitle is being replaced with the jobTitle of the user
user.phone is being replaced with the phone of the user
user.email is being replaced with the email of the user
mailbox.fromName is being replaced with the senders name of the mailbox

Ref #281
2019-10-10 18:44:14 +02:00
FreeScout
b339b89c4b Merge pull request #344 from frommMoritz/enhancement/error-free-installation
Fix undefined variable envConfig
2019-10-10 19:42:39 +03:00
Moritz Fromm
cca24c8cbd Set maxlength for DB Password input to 50
Ref #343
2019-10-10 17:43:44 +02:00
Moritz Fromm
67cc30305b Retrieve envConfig in EnvironmentController
Ref #343
2019-10-10 17:31:05 +02:00
FreeScout
53102be531 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2019-10-05 23:18:00 -07:00
FreeScout
6b85d2398c v1.3.7 2019-10-05 23:17:32 -07:00
FreeScout
7fd5ee818a Auto Bcc for mailbox - closes #59 2019-10-05 23:15:05 -07:00
FreeScout
04f149c072 Updated pt-PT translation 2019-10-05 22:23:15 -07:00
FreeScout
c3edfd8acf Fix attachment names 2019-10-05 22:19:01 -07:00
FreeScout
9a7fc7c65c Merge pull request #332 from TWithers/attachment_name
Slugified attachment file names
2019-09-18 14:33:06 +03:00
Tim Withers
cc131cbf60 Slugified attachment file names to remove adverse interaction in browser urls. 2019-09-17 09:28:44 -07:00
FreeScout
2c4e415a66 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2019-09-06 05:23:52 -07:00
FreeScout
9bc4c3a210 v1.3.6 2019-09-06 05:23:00 -07:00
FreeScout
b9198e5b28 Fix conversations pagination - closes #315 2019-09-06 03:57:08 -07:00
FreeScout
67a5c44d06 Replace mailbox password with asterisks in HTML 2019-09-06 01:12:54 -07:00
FreeScout
39485f692c Log error if can not save vars.js 2019-09-06 00:02:15 -07:00
FreeScout
6a7c2dd778 Show preview of the full message for the thread in Show Original modal 2019-09-05 07:47:12 -07:00
FreeScout
20750ebddc Fetch emails with empty Message-ID 2019-09-05 05:07:11 -07:00
FreeScout
b8cbcb8619 Fetch emails with empty body 2019-09-05 05:01:36 -07:00
FreeScout
37fb2b1fb3 Check if outgoing port is open when testing connection 2019-09-05 04:02:33 -07:00
FreeScout
6950394054 Show mailbox email in mailbox settings sidebar 2019-09-05 03:39:40 -07:00
FreeScout
a0ee00bc2a Add target _blank to thread content only 2019-09-05 02:48:05 -07:00
FreeScout
dd58bdc545 Fetch emails with empty Message-ID - closes #311 2019-09-05 02:40:17 -07:00
FreeScout
67d46cda28 Update readme 2019-09-05 01:21:59 -07:00
FreeScout
06e04e1333 Show error message on updating modules 2019-08-10 09:11:57 -07:00
FreeScout
e8b745384c Prevent saving messages with empty body when detecting reply body 2019-08-10 00:11:38 -07:00
FreeScout
5397175314 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2019-08-07 11:34:44 -07:00
FreeScout
62636d73dc v1.3.5 2019-08-07 11:33:51 -07:00
FreeScout
374498b445 Fix app.force_https option 2019-08-07 11:29:06 -07:00
FreeScout
d63bfacc10 Fix ajax URL in subdirectory installations 2019-08-07 11:05:22 -07:00
FreeScout
9d697647c3 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2019-08-03 04:56:49 -07:00
FreeScout
e674b71081 Return tools/update.sh script 2019-08-03 04:53:56 -07:00
FreeScout
2b9e94b596 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2019-08-03 04:39:40 -07:00
FreeScout
3411683e47 v1.3.4 2019-08-03 04:38:58 -07:00
FreeScout
e2ca7d73a6 Do not show own conversation views in the conversation list 2019-08-02 11:53:33 -07:00
FreeScout
f2f5a9c488 Fix saving user view with UT8 name - closes #303 2019-08-02 06:30:32 -07:00
FreeScout
b0063d2c2e Fix utf8 user/customer initials 2019-08-02 06:29:20 -07:00
FreeScout
c55162e745 Fix app.force_https option 2019-08-02 05:16:21 -07:00
FreeScout
7e590b2179 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2019-07-30 06:51:57 -07:00
FreeScout
d157e702c1 v1.3.3 2019-07-30 06:49:09 -07:00
FreeScout
5681f4b74a Show users who are viewing conversation 2019-07-30 06:43:32 -07:00
FreeScout
2344917ce7 Update styles 2019-07-30 06:07:29 -07:00
FreeScout
e42ed6f581 Search by customers 2019-07-30 01:45:34 -07:00
FreeScout
3633bb68d7 Truncate long mailbox emails in the list 2019-07-30 01:36:54 -07:00
FreeScout
c178a7890e Thread date as hyperlink 2019-07-30 01:02:51 -07:00
FreeScout
7fea640a84 Show users who are viewing conversation 2019-07-30 00:28:31 -07:00
FreeScout
dea9c2e998 Merge branch 'master' into collision_detection 2019-07-28 00:14:18 -07:00
FreeScout
53a97f07b8 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2019-07-27 09:07:20 -07:00
FreeScout
e0ceb2bcd7 v1.3.2 2019-07-27 09:06:39 -07:00
FreeScout
f57611c1c9 Conversation after subject hook 2019-07-27 06:30:04 -07:00
FreeScout
b5ab58790a Hide cc trigger in note 2019-07-27 06:14:27 -07:00
FreeScout
7c5ca832e8 Fix json encoding non-latin symbols 2019-07-27 06:07:04 -07:00
FreeScout
491d68088e jsonEncodeUtf8() helper function 2019-07-27 05:51:34 -07:00
FreeScout
c478556c67 Search filters 2019-07-27 05:51:05 -07:00
FreeScout
83d5b26922 nl2brDouble helper function 2019-07-27 00:03:35 -07:00
FreeScout
4810cea1ab Conversation thread hooks 2019-07-26 21:58:29 -07:00
FreeScout
73657915a3 Helper functions 2019-07-26 21:58:02 -07:00
FreeScout
cceb140eb1 Add remove format button to the default editor 2019-07-26 00:10:42 -07:00
FreeScout
f147b4ef5f Fix creating Phone conversation without draft 2019-07-25 23:49:25 -07:00
FreeScout
102c1fcd96 Do not try to send email to customer for Phone conversations - closes #296 2019-07-25 23:45:04 -07:00
FreeScout
48e89b36c2 New reply separator 2019-07-25 23:26:08 -07:00
FreeScout
d1386ee8f3 Move conversation feature 2019-07-25 23:01:03 -07:00
FreeScout
1c32dad7d5 Update readme 2019-07-25 04:05:39 -07:00
FreeScout
362e9cf1e6 Check mailbox when fetching replies 2019-07-24 23:58:00 -07:00
FreeScout
d13640c466 Fix cc trigger position when forwarding 2019-07-24 23:34:15 -07:00
FreeScout
36ace2bb7e If customer without name show first part of email in profile 2019-07-24 22:51:16 -07:00
FreeScout
533a482d2c Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2019-07-24 22:14:01 -07:00
FreeScout
14cbb06bdb v1.3.1 2019-07-24 22:13:06 -07:00
FreeScout
c2fb36a6d1 Optimize self-updating 2019-07-24 22:11:42 -07:00
FreeScout
0888a08c12 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2019-07-24 05:46:07 -07:00
FreeScout
8a6c81c607 Optimize self-updating 2019-07-24 05:35:20 -07:00
FreeScout
90bd50c38e Fix UL/OL in summernote editor 2019-07-24 05:13:34 -07:00
FreeScout
aad11d5d76 Redesign floating alerts 2019-07-24 05:03:29 -07:00
FreeScout
b9c140160c Show assignee or status change in note 2019-07-24 03:31:00 -07:00
FreeScout
497efafabf Redesign - customer in conversation on mobile 2019-07-24 03:26:31 -07:00
FreeScout
3e78fdc737 Redesign - blockquotes 2019-07-24 03:10:00 -07:00
FreeScout
a8b045d95b Fix icon position in floating message 2019-07-24 03:02:01 -07:00
FreeScout
ab9c5f68b8 Change logs route as on some hostings /logs/ not allowed 2019-07-24 02:46:28 -07:00
FreeScout
dc2ffe6393 Fix cc/bcc trigger position 2019-07-24 01:29:26 -07:00
FreeScout
7807a68a74 Allow to specify fetching period 2019-07-23 00:04:36 -07:00
FreeScout
99d11db2ed Fix error fetching emails from Office365 2019-07-22 23:34:01 -07:00
FreeScout
e1346ce833 Redesign modules list 2019-07-22 22:18:10 -07:00
FreeScout
a4e0fc66b4 Redesign modules list 2019-07-22 12:10:46 -07:00
FreeScout
046a00aaa0 Redesign modules list 2019-07-22 10:57:46 -07:00
FreeScout
08fe70bd69 Redesign modules list 2019-07-22 10:53:11 -07:00
FreeScout
df9b151d98 Update readme 2019-07-22 07:05:52 -07:00
FreeScout
ba12b17e94 Add vendor files to dist 2019-07-22 07:01:56 -07:00
FreeScout
911d18ed31 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2019-07-22 06:59:51 -07:00
FreeScout
b16e61af1a v1.3.0 2019-07-22 06:59:11 -07:00
FreeScout
41ad6bf3e0 Phone conversations 2019-07-22 06:56:01 -07:00
FreeScout
c16916455d Add native PHP mail() function support for swiftmailer 2019-07-22 02:39:52 -07:00
FreeScout
83376e0eac Phone conversations 2019-07-21 23:39:23 -07:00
FreeScout
95c2e1fea9 Select2 for Cc, Bcc, to_email 2019-07-16 05:52:09 -07:00
FreeScout
b38e0a4938 Send new conversation to multiple customers 2019-07-16 02:38:06 -07:00
FreeScout
5a3b15abcb Ajax suggest for recipient 2019-07-16 00:36:51 -07:00
FreeScout
e28ad0cc33 Fix MySQL isnt in GROUP BY error 2019-07-14 22:34:06 -07:00
FreeScout
9e2e10f7fb Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2019-07-13 22:12:37 -07:00
FreeScout
06f9842a3a v1.2.6 2019-07-13 22:11:49 -07:00
FreeScout
d477b34d27 Do not allow to open app at /public/ 2019-07-12 20:51:45 -07:00
FreeScout
999f2ab3b0 Do not allow to open app at /public/ 2019-07-12 13:49:52 -07:00
FreeScout
99f5f4a2ad Save attachments when creating a new conversation - closes #289 2019-07-12 07:31:23 -07:00
FreeScout
5d79e3c833 Forward attachmets - closes #72 2019-07-12 06:56:07 -07:00
FreeScout
29f1f86f03 Search filters 2019-07-12 03:47:12 -07:00
FreeScout
7ea5f15371 Search filters 2019-07-12 02:52:11 -07:00
FreeScout
a7fd5288a1 Add vendor files to dist 2019-07-09 02:30:13 -07:00
FreeScout
3c0c61cb97 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2019-07-09 02:26:43 -07:00
FreeScout
771e9cd92b v1.2.5 2019-07-09 02:25:31 -07:00
FreeScout
e4548ac167 Fix The specified charset is not supporte error in Microsoft mailboxes - closes #176 2019-07-09 02:06:28 -07:00
FreeScout
c5e28eea7a Fixed not applying mailbox mail config 2019-07-09 01:10:27 -07:00
FreeScout
a0e187d1c7 Update vars.js only if content changed to prevent minified file rebuild 2019-07-08 21:40:03 -07:00
FreeScout
650c9029e9 Show creator of the new conversation draft 2019-07-08 06:33:37 -07:00
FreeScout
611cb37878 Show user status in profile and in the dropdown 2019-07-08 06:04:02 -07:00
FreeScout
2d54d40149 Optimize DB queries on conversation page 2019-07-08 03:41:25 -07:00
FreeScout
58493c77c1 Allow to send ougoing emails from SMTP server without login and password 2019-07-08 02:51:02 -07:00
FreeScout
78f28e8ed3 Update vars.js only if content changed to prevent minified file rebuild 2019-07-08 02:48:50 -07:00
FreeScout
5625442913 Do not regenerate vars on Setting save 2019-07-08 02:27:18 -07:00
FreeScout
6961d8d9ac Set editor buttons height 2019-07-06 22:13:48 -07:00
FreeScout
24b36271fe Hide todos 2019-07-06 22:12:56 -07:00
FreeScout
0bb3ec0d02 Ignore false IMAP errors 2019-07-06 02:13:40 -07:00
FreeScout
53ba824106 Ignore false IMAP errors - closes #270 2019-07-06 00:12:14 -07:00
FreeScout
396942fd17 Fix foreach issue in Webklex/IMAP/Client.php in getFolder() 2019-07-05 04:52:27 -07:00
FreeScout
8a92c6c44e Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2019-07-05 03:20:40 -07:00
FreeScout
0151b7af4f v1.2.4 2019-07-05 03:19:50 -07:00
FreeScout
06ccb07c09 Fetch emails from custom IMAP folders - closes #206 2019-07-05 03:18:15 -07:00
FreeScout
25a00f5585 New email reply separator 2019-07-04 23:56:50 -07:00
FreeScout
2b0156fb13 Fix vars js issue 2019-07-04 23:51:18 -07:00
FreeScout
88aac4accf Do not force clear cache on Status page queue:work is not running 2019-07-04 23:46:04 -07:00
FreeScout
171be5630c Note should always keep conversation status 2019-07-04 23:31:31 -07:00
FreeScout
c4b6011dd9 Fix wrong assignee on reply 2019-07-04 23:22:54 -07:00
FreeScout
b90e68f23c Respect Users are allowed to edit notes/replies setting 2019-07-04 22:46:38 -07:00
FreeScout
61fdf8150b Add vendor files to dist 2019-07-04 07:35:30 -07:00
FreeScout
09ffb53bed Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2019-07-04 07:32:57 -07:00
FreeScout
a0ee0a7387 Update composer.json 2019-07-04 07:32:38 -07:00
FreeScout
5f048f67bb Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2019-07-04 07:03:43 -07:00
FreeScout
6b8a915609 Update composer.json - fix packages versions 2019-07-04 07:02:14 -07:00
FreeScout
d6aad6f97e Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2019-07-04 06:53:39 -07:00
FreeScout
38485396ff v1.2.3 2019-07-04 06:52:12 -07:00
FreeScout
6e37cdb862 Fix vars js issue 2019-07-04 06:50:27 -07:00
FreeScout
fc82c1a1a0 Update readme 2019-07-04 06:05:57 -07:00
FreeScout
db9103c568 Show detailed error message on updating error 2019-07-04 06:03:41 -07:00
FreeScout
77ab67edcd Show clear cache output in tools.php 2019-07-03 06:12:46 -07:00
FreeScout
f24bd71576 Beautify exception message 2019-07-03 05:49:52 -07:00
FreeScout
5638b3b0ae Hooks in login form 2019-07-03 04:01:15 -07:00
FreeScout
76a1dbd8f1 Custom middleware 2019-07-03 04:00:55 -07:00
FreeScout
71464ab1fe Give names to web routes 2019-07-03 03:59:31 -07:00
FreeScout
6698957104 Fix recipient dropdown when there are other customers in conversation 2019-07-02 22:22:00 -07:00
FreeScout
8eb47b8b69 Detect reply body in Outlook emails 2019-07-02 22:09:29 -07:00
FreeScout
7e9edd5d21 Exclude mailbox emails from recipients on reply 2019-07-02 10:55:11 -07:00
FreeScout
12c11e88d5 Update readme 2019-07-02 09:46:34 -07:00
FreeScout
f653dbb8f9 Fix Eventy Too few arguments error 2019-07-02 06:12:01 -07:00
FreeScout
9e469414b3 Fix displaying To field when forwarding - closes #72 2019-07-02 03:26:42 -07:00
FreeScout
0d3b88c33b Detect reply body in Outlook emails 2019-07-02 02:55:31 -07:00
FreeScout
57ae95616f Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2019-07-01 09:30:28 -07:00
FreeScout
b6f5c240e0 Update axios version in package.json 2019-07-01 09:30:07 -07:00
FreeScout
93810fa81a Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2019-07-01 09:27:21 -07:00
FreeScout
a4fdf03dbd v1.2.2 2019-07-01 09:26:15 -07:00
FreeScout
4d058110e0 Store vars.js in /storage/js/vars.js 2019-07-01 05:39:31 -07:00
FreeScout
fffedb608b Improve reply detection on fetching 2019-07-01 03:58:44 -07:00
FreeScout
a20b142014 Update default mailbox signature 2019-07-01 03:58:24 -07:00
FreeScout
9f672322e0 Collision detection 2019-07-01 02:12:25 -07:00
FreeScout
1c8c1a0ba9 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2019-06-27 06:12:44 -07:00
FreeScout
bfbdb3681b Update composer.json 2019-06-27 06:11:45 -07:00
FreeScout
b9f3d8736e Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2019-06-27 01:57:34 -07:00
FreeScout
290d6d8f54 v1.2.1 2019-06-27 01:56:50 -07:00
FreeScout
0e470df4b6 Fix checking DB connection on installation - closes #279 2019-06-27 01:02:51 -07:00
FreeScout
68588d1bb2 Fix drafts issue - closes #234 2019-06-27 00:17:50 -07:00
FreeScout
3cd7d390c1 Fix Assign to value 2019-06-26 06:21:11 -07:00
FreeScout
a0bf3c4d1b Restart queue:work after changing mailbox sending settings 2019-06-26 05:53:59 -07:00
FreeScout
2cb9e0efbd Update customer email template 2019-06-26 05:26:50 -07:00
FreeScout
5b81cc24e7 Update email delivery services list 2019-06-26 00:23:35 -07:00
FreeScout
b6c53c8795 Fix Data too long for column status_message - closes #259 2019-06-25 10:58:58 -07:00
FreeScout
0aef81100f Show mailbox name in user notification 2019-06-25 00:30:17 -07:00
FreeScout
137e7c0b7b Hide mailbox name in user notification 2019-06-25 00:25:11 -07:00
FreeScout
b87892dc9b Add vendor files to dist 2019-06-24 22:00:06 -07:00
FreeScout
5a640831e2 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2019-06-24 21:57:49 -07:00
FreeScout
c73b11f62c v1.2.0 2019-06-24 21:56:53 -07:00
FreeScout
12cd3c0af7 Forwarding conversations - undoing 2019-06-24 04:51:40 -07:00
FreeScout
51a50a44a4 Restore attachments when editing draft 2019-06-24 02:14:23 -07:00
FreeScout
1e3c024b87 Forwarding conversations - closes #72 2019-06-24 01:15:58 -07:00
FreeScout
1c0c1db43b Forwarding conversations 2019-06-23 22:39:30 -07:00
FreeScout
fac3713af4 Fix Class Helper not found in translation-manager.php 2019-06-21 22:42:00 -07:00
FreeScout
a709f67a00 Fix sending emails to customers with multiple emails - closes #258 2019-06-20 23:36:31 -07:00
FreeScout
790aea27db Update readme 2019-06-20 23:02:23 -07:00
FreeScout
f0af4ea870 Fix URL validation pattern 2019-06-20 22:38:59 -07:00
FreeScout
153ecd726e Prevent saving draft of the message which has been sent 2019-06-20 05:55:58 -07:00
FreeScout
dff86e679c Update readme 2019-06-20 03:43:10 -07:00
FreeScout
2bb93938f9 Edit thread 2019-06-20 03:37:43 -07:00
FreeScout
48e16a54d3 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2019-06-19 23:20:19 -07:00
FreeScout
676652fa09 v1.1.10 2019-06-19 23:19:35 -07:00
FreeScout
294df88809 Take mailbox Validate Certificate settings into account on checking IMAP connection - closes #269 2019-06-19 22:49:06 -07:00
FreeScout
d815de5131 Fix subdirectory paths 2019-06-19 06:00:30 -07:00
FreeScout
32a156c3a6 Fix mailbox email in the sidebar 2019-06-18 23:16:30 -07:00
FreeScout
ed77f937f4 Add bottom margins to settings forms 2019-06-18 02:04:34 -07:00
FreeScout
7070331706 Update readme 2019-06-18 01:57:06 -07:00
FreeScout
3f82986e2c Update readme 2019-06-18 01:48:34 -07:00
FreeScout
7dfbaad817 Fix saving settings in subdirectory installation 2019-06-18 01:43:56 -07:00
FreeScout
4b8a38d7fe Fix fonts and icons paths in CSS 2019-06-17 22:37:32 -07:00
FreeScout
fee5e94239 Add vendor files to dist 2019-06-16 04:17:41 -07:00
FreeScout
57d89b371d Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2019-06-16 04:15:47 -07:00
FreeScout
e5ecc679f0 v1.1.9 2019-06-16 04:14:41 -07:00
FreeScout
8e32f5db50 List of email delivery services 2019-06-16 04:05:56 -07:00
FreeScout
edde3c4507 Validate Certificate option for fetching emails - closes #253 #269 2019-06-16 03:27:23 -07:00
FreeScout
ae4e35ef65 Allow installation in a subdirectory 2019-06-16 00:23:17 -07:00
FreeScout
b8633c7711 Allow installation in a subdirectory 2019-06-15 23:47:26 -07:00
FreeScout
cbccc495a9 Allow installation into subdirectory 2019-06-15 08:27:33 -07:00
FreeScout
9d0702800c Allow installation into subdirectory 2019-06-15 07:52:34 -07:00
FreeScout
04b28811a4 Fix Call to a member function url() on null in thread_by.php 2019-06-09 19:33:09 -07:00
FreeScout
bb7c6db8fb Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2019-06-05 20:39:46 -07:00
FreeScout
adf3f8f21d v1.1.8 2019-06-05 20:38:47 -07:00
FreeScout
8e0b388fd9 Portugues Brasil translation 2019-06-05 20:33:48 -07:00
FreeScout
d466e62649 Add languages to Readme 2019-06-05 04:15:41 -07:00
FreeScout
307742fb81 Portuguese translation - closes #239 2019-06-05 04:10:37 -07:00
FreeScout
b6c76dd6ed Make folders names translatable - closes #262 2019-06-05 03:17:44 -07:00
FreeScout
e5fb71cf24 Merge pull request #231 from guestisp/fix-195
Fix #195
2019-06-05 15:50:34 +05:45
FreeScout
951765a711 Store Fetch Mail schedule option as app.fetch_schedule 2019-06-05 02:59:39 -07:00
FreeScout
2165cd8be8 Merge pull request #261 from Junker/fetch-schedule-setting
fetch mail schedule setting
2019-06-05 15:23:00 +05:45
FreeScout
c66dcf1bb6 Conversation filter in FetchEmails 2019-06-05 01:16:46 -07:00
FreeScout
30941fc4c0 Do not send notification if conversation is spam 2019-06-04 02:44:20 -07:00
FreeScout
d7d279c7b6 stripTags helper function 2019-06-04 01:13:34 -07:00
FreeScout
6afaa4a809 4th parameter for conversation.status_changed_by_user action 2019-06-04 01:13:06 -07:00
FreeScout
99a0d69269 Private disk for Storage to save files in app/storage 2019-06-04 01:12:16 -07:00
FreeScout
0db95d4f15 Conversation function getFirstThread() 2019-06-03 03:06:42 -07:00
FreeScout
676c431bb2 Do not send notification if conversation is spam 2019-06-02 23:03:17 -07:00
FreeScout
d969b567b3 Remove hardcoded action descriptions 2019-06-02 22:52:37 -07:00
FreeScout
46dfca7865 Added getFolderByType() function to Mailbox 2019-06-02 22:02:58 -07:00
FreeScout
e9c19e26ba Conversation filter in FetchEmails 2019-06-02 22:02:13 -07:00
FreeScout
a2d9b608d1 Not Spam button - stay on the same page 2019-05-10 03:04:58 -07:00
FreeScout
6f64c14ef1 Update readme 2019-05-07 23:14:04 -07:00
FreeScout
4b492d0013 customer_profile.menu hook 2019-05-07 23:11:05 -07:00
FreeScout
02d682c50d Prev status in conversation.status_changed_by_user hook 2019-05-07 22:03:51 -07:00
FreeScout
033283810f Not Spam button 2019-05-07 04:06:47 -07:00
FreeScout
35a02a6816 Redesign - labels 2019-05-06 03:52:22 -07:00
FreeScout
ade6beaffc Static function conversationUrl 2019-05-06 03:30:14 -07:00
FreeScout
e377d5143e customer.profile_data hook 2019-05-06 02:47:45 -07:00
Junker
2ff723f54b fetch-monitor depends on 'fetch_schedule' setting 2019-05-02 22:45:36 +02:00
Junker
504398e5d7 rename setting 'mail_schedule' to 'fetch_schedule'. Add APP_FETCH_SCHEDULE env param. 2019-05-02 22:44:23 +02:00
FreeScout
af57cd0020 Hide Improve Delivery section from mailbox connection settings 2019-05-01 21:46:17 -07:00
FreeScout
399a815526 Show number of conversations in Spam folder 2019-05-01 21:44:38 -07:00
Junker
ff2591550e fetch mail schedule setting 2019-05-01 18:09:03 +02:00
FreeScout
34e3c8d6d9 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2019-04-25 22:05:36 -07:00
FreeScout
8f4d942ced v1.1.7 2019-04-25 22:05:02 -07:00
FreeScout
587623a329 Redesign 2019-04-25 22:03:05 -07:00
FreeScout
bce1220229 Fix redirect to Mine folder after delete conversation in Mine folder 2019-04-25 21:59:45 -07:00
FreeScout
e4c10ccd73 Merge pull request #249 from Junker/fix-delete-conversation-from-mine-folder
fix: redirect back to 'Mine' folder after delete conversation in 'Mine' folder
2019-04-26 10:35:55 +05:45
FreeScout
5e3cba47da Redesign - show mailbox email under title 2019-04-25 21:48:53 -07:00
FreeScout
6bf0210a63 Function to check if user has access to a particular mailbox 2019-04-25 21:27:26 -07:00
FreeScout
ad50d26e0c Helper function to trim non-breaking spaces 2019-04-25 21:26:37 -07:00
FreeScout
941d5171d3 Hooks in user email notifications 2019-04-25 21:26:08 -07:00
FreeScout
8713a9cb97 Update readme 2019-04-25 21:23:59 -07:00
Junker
d434181fd6 fix: redirect to 'mine' folder after delete conversation in 'mine' folder 2019-02-28 07:08:17 +01:00
Alessandro Corbelli
287ca5cbc2 Fix #195 2019-01-10 10:43:36 +01:00
FreeScout
6670ff99cc Merge pull request #230 from guestisp/fix-229 Show error message if user has no permissions to delete conversation
Show error message if user has no permissions to delete conversation
2019-01-10 10:44:04 +03:00
Alessandro Corbelli
cbd71b7dd9 StyleCI 2019-01-09 14:37:09 +01:00
Alessandro Corbelli
ce2c06af06 StyleCI 2019-01-09 14:36:43 +01:00
Alessandro Corbelli
fa62292633 Fix #229 2019-01-09 14:36:02 +01:00
FreeScout
3c6325d7a0 Merge pull request #225 from Junker/conversation-view-action-events new action events for conversation view
new action events for conversation view
2019-01-06 08:10:00 +03:00
Junker
c90e2f9e27 new action events for conversation view 2019-01-05 18:19:19 +01:00
FreeScout
705538582f Merge pull request #223 from Junker/fix-fetch-email-bounce-check fix email bounce check in fetch-emails script
fix email bounce check in fetch-emails script
2019-01-05 07:39:52 +03:00
Junker
2c974afecc fix email bounce check in fetch-emails script 2019-01-04 23:04:31 +01:00
FreeScout
f96a78cb82 Logging on bounce detection 2019-01-04 03:40:34 -08:00
FreeScout
b77e268441 Merge pull request #218 from guestisp/bounce-detection-improvements Bounce detection improvements
Bounce detection improvements
2019-01-04 14:32:03 +03:00
Alessandro Corbelli
b17fccfed0 Removed logs 2019-01-02 12:10:32 +01:00
FreeScout
0ec8a2a942 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2019-01-01 23:01:47 -08:00
FreeScout
24629b8fb4 v1.1.6 2019-01-01 23:00:51 -08:00
FreeScout
9526c6714a Update note_added hook 2018-12-31 01:08:33 -08:00
FreeScout
2471c37fd8 Create thread function 2018-12-31 00:22:43 -08:00
FreeScout
cc78c0e560 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2018-12-28 22:32:17 -08:00
FreeScout
2cb7d81229 v1.1.5 2018-12-28 22:31:34 -08:00
FreeScout
476f120683 Fix switch to note link 2018-12-28 20:58:13 -08:00
FreeScout
72638891fe Redesign 2018-12-28 06:08:23 -08:00
FreeScout
16a7edc0ca Redesign 2018-12-28 05:07:33 -08:00
FreeScout
2fc1b7d3e0 Fix showing attachments in drafts 2018-12-28 04:56:14 -08:00
FreeScout
7069b8e178 Redesign 2018-12-28 04:36:06 -08:00
FreeScout
0cb9961501 Improve background process stucking detection 2018-12-26 23:53:54 -08:00
FreeScout
33dc39d4ad Redesign 2018-12-26 23:04:04 -08:00
FreeScout
5246e77c60 Note should not change conversation status 2018-12-26 23:01:26 -08:00
Alessandro Corbelli
def039f591 More log added 2018-12-22 11:51:07 +01:00
FreeScout
b91e1924e0 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2018-12-21 21:02:22 -08:00
FreeScout
da25e7c359 v1.1.4 2018-12-21 21:01:08 -08:00
Alessandro Corbelli
2a860590d8 Detect bounce from Return-Path 2018-12-21 18:33:33 +01:00
Alessandro Corbelli
0be7557204 Increased log 2018-12-21 18:14:56 +01:00
FreeScout
46b596af8c Merge pull request #212 from freescout-helpdesk/analysis-XWekbe
Apply fixes from StyleCI
2018-12-21 12:11:09 +03:00
FreeScout
90b9254ffb Apply fixes from StyleCI 2018-12-21 09:10:07 +00:00
FreeScout
c74bd56eb3 Set conversation preview when thread is created from draft 2018-12-21 01:09:07 -08:00
FreeScout
d2cdca1f6a Remove script and style tags content from conversation preview 2018-12-21 01:08:47 -08:00
FreeScout
6c584b5c70 Redesign 2018-12-21 00:29:48 -08:00
FreeScout
b9d8540f23 Update pending icon 2018-12-21 00:05:45 -08:00
FreeScout
8adc269058 Merge pull request #188 from guestisp/add-should_save_thread-hook should_save_thread hook
Add "fetch_emails.should_save_thread" hook
2018-12-21 10:49:05 +03:00
FreeScout
416b56aed8 Extend purifier table tags 2018-12-20 23:46:13 -08:00
FreeScout
bd268fd315 Merge pull request #207 from Junker/email-allow-table-tag show table tag in conversation body
show table tags(table,tr,td) in conversation email body
2018-12-21 10:42:08 +03:00
FreeScout
9f391b59fc Remove message/rfc822 contetn type from bounce detection 2018-12-20 23:36:12 -08:00
FreeScout
277b683d98 mailbox.update.after_ticket_status action 2018-12-20 22:28:12 -08:00
FreeScout
150da8a98d Merge pull request #204 from guestisp/add-hook-for-customize-mailbox-update-page Call hook in mailbox update page
Call hook in mailbox update page
2018-12-21 09:19:05 +03:00
FreeScout
4ea0897e3c mailbox.fillable_fields filter 2018-12-20 22:15:41 -08:00
FreeScout
278ecff4ec Merge pull request #203 from guestisp/add-hook-for-customizing-fillable-fields Allow customizing the Mailbox fillable fields by module developers
Allow customizing the Mailbox fillable fields by module developers
2018-12-21 09:07:43 +03:00
Junker
326d37cf1c show table tags(table,tr,td) in conversation email body 2018-12-20 13:29:51 +01:00
Alessandro Corbelli
3c704eec2f Call hook in mailbox update page to allow module developers customize the mailbox update page 2018-12-19 15:40:45 +01:00
Alessandro Corbelli
9b5e4acda0 Allow customizing the Mailbox fillable fields by module developers 2018-12-19 15:36:32 +01:00
Alessandro Corbelli
456cb8491f StyleCI 2018-12-19 14:17:37 +01:00
Alessandro Corbelli
d65ce800af StyleCI 2018-12-19 14:17:03 +01:00
Alessandro Corbelli
a236eb2e11 Replaced $hook object with $data array
Added new hook to allow module developers to change data saved by fetchemails
Replaced fetch_emails.should_save_thread to only use a boolean
2018-12-19 14:15:45 +01:00
Alessandro Corbelli
39c6242f3e Merge branch 'master' into add-should_save_thread-hook 2018-12-19 14:09:22 +01:00
FreeScout
d1e5604e79 Fix deleting conversation having a draft thread 2018-12-18 23:56:56 -08:00
FreeScout
098d3572d6 Fix bulk delete modal layout 2018-12-18 23:34:40 -08:00
FreeScout
d61878a761 Merge pull request #198 from guestisp/fix-197 has_attachments not updated
Fix #197
2018-12-19 10:03:54 +03:00
FreeScout
575b8e43ad Remove tag button from bulk actions panel 2018-12-18 23:00:18 -08:00
FreeScout
22eda49187 Merge pull request #194 from guestisp/fix-191 Select All checkbox doesnt work
Fix #191
2018-12-19 09:57:35 +03:00
FreeScout
0a52c0b542 Merge pull request #193 from guestisp/fix-192 Trying to get property email of non-object
Fix #192
2018-12-19 09:52:59 +03:00
FreeScout
aa1c26a125 Merge pull request #202 from guestisp/fix-201
Manage menu should be removed if no privilges are available
2018-12-19 09:46:27 +03:00
FreeScout
30bbef8285 Automatically recalculate folders counters 2018-12-18 22:38:42 -08:00
FreeScout
74e8e51a43 Merge pull request #200 from guestisp/fix-187
Update folder counters command
2018-12-19 09:26:29 +03:00
Alessandro Corbelli
db64fd2aeb Fix #201 2018-12-18 17:41:29 +01:00
Alessandro Corbelli
fc267329d4 Fix #187 2018-12-18 17:17:07 +01:00
Alessandro Corbelli
fe54b5af4b StyleCI 2018-12-18 17:15:20 +01:00
Alessandro Corbelli
677bca506f Fix #197 2018-12-18 16:21:00 +01:00
Alessandro Corbelli
4371399b41 Fix #191 2018-12-18 12:46:51 +01:00
Alessandro Corbelli
284f0835da Fix #192 2018-12-18 12:40:30 +01:00
Alessandro Corbelli
2b687f3334 StyleCI 2018-12-18 12:35:02 +01:00
Alessandro Corbelli
cf1e89f6ef StyleCI 2018-12-18 12:34:27 +01:00
Alessandro Corbelli
057b814e91 Fix FetchEmails 2018-12-18 12:33:31 +01:00
Alessandro Corbelli
2022bf021e StyleCI 2018-12-17 17:57:41 +01:00
Alessandro Corbelli
7b1cb4d29a StyleCI 2018-12-17 17:50:54 +01:00
Alessandro Corbelli
81e80d30f6 Add "fetch_emails.should_save_thread" hook 2018-12-17 17:45:09 +01:00
FreeScout
77bcdd76c8 On update copy new vendor and rename to avoid error 2018-12-15 21:35:49 -08:00
FreeScout
49cced03c2 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2018-12-15 20:55:50 -08:00
FreeScout
1ed2448c7d v1.1.3 2018-12-15 20:54:57 -08:00
FreeScout
e497c95ff1 Mailbox name in notification to user 2018-12-15 06:00:48 -08:00
FreeScout
5fd1ea35fc Show delivery error message in conversation thread 2018-12-15 05:24:21 -08:00
FreeScout
f54e61dae2 Improve bounce detection 2018-12-15 01:38:53 -08:00
FreeScout
a6fe1ecf87 Redesign 2018-12-15 01:25:44 -08:00
FreeScout
e5bfe957e2 Show delivery error message in conversation thread 2018-12-15 01:15:30 -08:00
FreeScout
36d52d45ef Merge pull request #184 from guestisp/bounce-detection-improvement
Bounce detection improvement
2018-12-15 08:40:05 +03:00
FreeScout
148e89e231 Comments for the process of retrying on send error 2018-12-14 21:28:12 -08:00
FreeScout
19fba8da7a Show release details link 2018-12-14 04:16:09 -08:00
FreeScout
1d69e0052c Check if .env file is writable in System Status 2018-12-14 02:01:03 -08:00
Alessandro Corbelli
0022c3ca15 StyleCI 2018-12-14 10:26:38 +01:00
Alessandro Corbelli
6d3ec46c61 StyleCI 2018-12-14 10:26:04 +01:00
Alessandro Corbelli
211768717e StyleCI 2018-12-14 10:25:21 +01:00
Alessandro Corbelli
8a47b94cdb StyleCI 2018-12-14 10:24:36 +01:00
Alessandro Corbelli
d7a3829a4d More improvements to bounce detection 2018-12-14 10:22:38 +01:00
FreeScout
a125a8d7b7 Do not send auto replies to bounces 2018-12-14 01:10:43 -08:00
Alessandro Corbelli
9b2495edce Small improvement to bounce detection 2018-12-14 09:48:36 +01:00
FreeScout
fd815acf72 Improve bounce detection 2018-12-14 00:18:23 -08:00
FreeScout
6b2071e405 Accept auto responders but do not send auto replies 2018-12-13 22:41:30 -08:00
FreeScout
503bd57d5e Merge pull request #180 from freescout-helpdesk/analysis-8j2yAZ
Apply fixes from StyleCI
2018-12-13 12:11:42 +03:00
FreeScout
d8fbf21579 Apply fixes from StyleCI 2018-12-13 09:11:15 +00:00
FreeScout
9145463243 Editor list 2018-12-13 01:06:20 -08:00
FreeScout
0052b0b62c Show release details link 2018-12-12 23:38:57 -08:00
FreeScout
7f25e75f50 Proper action description name when adding a note 2018-12-12 22:51:02 -08:00
FreeScout
46add87243 Fix last reply by in conversations 2018-12-12 22:40:23 -08:00
FreeScout
78eed5c405 Fix saving settings in env file 2018-12-12 22:00:56 -08:00
FreeScout
574d6bbf8c Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2018-12-12 21:14:10 -08:00
FreeScout
021b118916 Replace const with number in user migration 2018-12-12 21:13:49 -08:00
FreeScout
53fc5969db Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2018-12-12 20:53:50 -08:00
FreeScout
338c89c53b Redesign 2018-12-12 11:12:41 -08:00
FreeScout
8713e19d78 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2018-12-12 06:39:48 -08:00
FreeScout
9a769154d7 Logs monitoring in Settings - Alerts 2018-12-12 06:39:24 -08:00
FreeScout
ab707bd365 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2018-12-12 06:07:40 -08:00
FreeScout
b6c1daa25e v1.1.2 2018-12-12 06:07:04 -08:00
FreeScout
72c6806fa7 Redesign 2018-12-12 06:06:18 -08:00
FreeScout
d74af3e5c0 Show original message for outgoing thread 2018-12-12 04:31:45 -08:00
FreeScout
adeb8c08f7 Redesign 2018-12-12 04:24:39 -08:00
FreeScout
794bdc2c40 Add target blank to links in threads 2018-12-12 02:36:49 -08:00
FreeScout
3b575c10d1 Redesign 2018-12-12 02:29:09 -08:00
FreeScout
7bba857251 Redesign 2018-12-12 01:41:28 -08:00
FreeScout
b59d8a663c Fix sending draft and reply at the same time 2018-12-12 01:38:25 -08:00
FreeScout
680245e0a9 Redesing 2018-12-12 01:12:40 -08:00
FreeScout
da54367245 Redesign 2018-12-12 00:23:36 -08:00
FreeScout
2b529eb70a Manage mailboxes link in the dashboard 2018-12-11 22:57:13 -08:00
FreeScout
9c19bca5d9 Delete user using flag 2018-12-11 22:43:58 -08:00
FreeScout
2b6ac6a375 Update conversation folder when adding a note 2018-12-11 01:07:34 -08:00
FreeScout
7e4aa4552c Redesign 2018-12-11 00:58:09 -08:00
FreeScout
50a8c8db8a Fix conversation preview updating 2018-12-10 23:41:14 -08:00
FreeScout
84685d1972 Redesign 2018-12-10 23:14:40 -08:00
FreeScout
caf8b26202 Redesign 2018-12-10 22:51:20 -08:00
FreeScout
946132e4b5 Logs monitoring in Settings - Alerts 2018-12-10 04:52:28 -08:00
FreeScout
b9b829db7f Fix getOptions() default value 2018-12-10 00:58:39 -08:00
FreeScout
02bd76ac57 Redesign 2018-12-09 23:28:35 -08:00
FreeScout
9d79f4aea4 Allow to retry and delete failed jobs in System Status 2018-12-09 06:13:00 -08:00
FreeScout
793aaac2e1 Redesign 2018-12-09 02:48:57 -08:00
FreeScout
1d8f79f3a2 Retry failed email jobs 2018-12-09 01:53:36 -08:00
FreeScout
8e978b7b17 Redesign 2018-12-09 01:53:14 -08:00
FreeScout
b5325c9641 QQ reply separator 2018-12-08 23:18:19 -08:00
FreeScout
414ab76cb4 QQ reply separator 2018-12-08 23:15:39 -08:00
FreeScout
ed06f38ddd Redesign 2018-12-08 23:13:18 -08:00
FreeScout
bf7e38a164 Italian translation 2018-12-08 21:21:48 -08:00
FreeScout
c4d58214b6 Redesign 2018-12-08 20:55:00 -08:00
FreeScout
84006b6923 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2018-12-08 05:05:51 -08:00
FreeScout
f69fe6a2e0 v1.1.1 2018-12-08 05:04:43 -08:00
FreeScout
d4e3e73c0d Redesign 2018-12-08 04:53:52 -08:00
FreeScout
35e56b4464 Update conversation preview on reply 2018-12-08 04:15:04 -08:00
FreeScout
6f8c002349 Update conversation preview on reply 2018-12-08 02:14:37 -08:00
FreeScout
40771d962e Fix counters for starred conversations and drafts 2018-12-08 01:00:20 -08:00
FreeScout
2e0567ef87 Redesign 2018-12-08 00:05:30 -08:00
FreeScout
ef24f2fdf8 Fix required password field on user create 2018-12-07 23:42:35 -08:00
FreeScout
456aeb3b8c Redesign 2018-12-07 22:31:55 -08:00
FreeScout
4e0a73623e Redesign 2018-12-07 22:06:42 -08:00
FreeScout
f0498fdd7f Redesign 2018-12-07 21:21:22 -08:00
FreeScout
d79446bf56 Redesign 2018-12-07 06:09:18 -08:00
FreeScout
acb4610b22 Fix in_password commit 2018-12-07 04:58:26 -08:00
FreeScout
d597dee6f7 Merge branch 'master' of github.com:freescout-helpdesk/freescout 2018-12-07 04:55:34 -08:00
FreeScout
7f5c7c3eaf Merge pull request #172 from guestisp/set-in_password-as-nullable
Add nullable to in_password field in mailboxes table
2018-12-07 15:55:12 +03:00
FreeScout
002bac584b Redesign 2018-12-07 04:52:37 -08:00
FreeScout
120131998e Redesign 2018-12-07 02:28:08 -08:00
Alessandro Corbelli
c5cdcdfce2 Removed unneeded migration
Fix nullable in previous migration
2018-12-07 10:55:21 +01:00
FreeScout
c7210de18f Redesign 2018-12-07 01:34:05 -08:00
Alessandro Corbelli
d3ce3e8722 Fix styleci 2018-12-06 12:47:51 +01:00
Alessandro Corbelli
0e79c82013 Fix styleci 2018-12-06 12:47:25 +01:00
Alessandro Corbelli
7c8a25cd4c Add nullable to in_password field in mailboxes table 2018-12-06 11:03:03 +01:00
FreeScout
9b83083bc3 Create conversation from thread 2018-12-06 01:13:04 -08:00
FreeScout
bd063db247 Redesign 2018-12-06 00:05:14 -08:00
FreeScout
65d4e4b8fe Redesign 2018-12-05 23:18:54 -08:00
FreeScout
2373771be8 Show waiting since time in dashboard 2018-12-05 04:33:05 -08:00
FreeScout
4d753b60d3 Fix active assigned conversations count in dashboard 2018-12-05 01:55:37 -08:00
FreeScout
c73ff62369 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2018-12-05 00:26:53 -08:00
FreeScout
570a9cca91 v1.1.0 2018-12-05 00:25:53 -08:00
FreeScout
101ea0ff9a Redesign 2018-12-05 00:08:28 -08:00
FreeScout
40026f4809 Merge branch 'master' of github.com:freescout-helpdesk/freescout 2018-12-04 23:40:27 -08:00
FreeScout
8c44e581f3 Redesign 2018-12-04 23:40:15 -08:00
FreeScout
7eb9caa96c Merge pull request #171 from freescout-helpdesk/analysis-XWkBly
Apply fixes from StyleCI
2018-12-05 10:07:29 +03:00
FreeScout
5c495fd1a5 Apply fixes from StyleCI 2018-12-05 07:06:54 +00:00
FreeScout
471920a0ba User data in signature 2018-12-04 23:05:27 -08:00
FreeScout
1a365ae247 Merge pull request #170 from taoeffect/master
Adds user related variables to signature (Closes #146)
2018-12-05 09:16:49 +03:00
Greg Slepak
16b7e407a0 Adds user related variables to signature (Closes #146) 2018-12-04 19:23:31 -05:00
FreeScout
9bdca967bb Redesign 2018-12-04 06:36:23 -08:00
FreeScout
79bd2f132c Redesign 2018-12-04 05:35:09 -08:00
FreeScout
f42b3c009e Detect and ignore autoresponders 2018-12-04 05:27:10 -08:00
FreeScout
80a765cf0c Check app requried version on module update 2018-12-04 02:43:48 -08:00
FreeScout
c97ec28ecd Redesign 2018-12-04 01:37:24 -08:00
FreeScout
f5ba15269d Update translations in vars.js 2018-12-04 01:09:41 -08:00
FreeScout
51f5149cb6 Fix saving arrays in Settings 2018-12-03 05:48:20 -08:00
FreeScout
e08476b3f8 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2018-12-03 05:06:28 -08:00
FreeScout
bcdf977753 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2018-12-03 05:04:52 -08:00
FreeScout
233dafa67b v1.0.9 2018-12-03 05:03:57 -08:00
FreeScout
8a3af78076 Redesign 2018-12-03 04:52:37 -08:00
FreeScout
f3e616b4a8 Helper functions 2018-12-03 04:52:28 -08:00
FreeScout
33a1374aea Highlight menu item when viewing folder 2018-12-03 02:06:29 -08:00
FreeScout
39cd69c439 Fix UserCreatedConversation event 2018-12-03 02:04:35 -08:00
FreeScout
bbe628e82f Helper functions 2018-12-03 01:03:23 -08:00
FreeScout
a375f96969 Redesign 2018-12-03 01:03:09 -08:00
FreeScout
4682e2df3f Show current date in logs 2018-12-03 00:57:50 -08:00
FreeScout
ba8d7daf53 Show current date in logs 2018-12-02 06:23:10 -08:00
FreeScout
3a4d459872 Redesign 2018-12-02 04:49:02 -08:00
FreeScout
60ee9ff640 Execute background action helper function 2018-12-02 04:47:47 -08:00
FreeScout
b673f1ef4f Redesign - favicon 2018-12-01 21:37:29 -08:00
FreeScout
d8828cab65 Redesign - favicon 2018-12-01 21:30:47 -08:00
FreeScout
52fb8f11a7 Fix Open Mailbox link in dashboard 2018-12-01 21:30:21 -08:00
FreeScout
05f13135a4 Redesign 2018-12-01 01:30:29 -08:00
FreeScout
700fa91850 Add vendor files to dist 2018-12-01 00:36:56 -08:00
FreeScout
6ba2f882e7 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2018-12-01 00:35:46 -08:00
FreeScout
f025df8c8c v1.0.8 2018-12-01 00:34:44 -08:00
FreeScout
79ff47410e French translation 2018-12-01 00:34:23 -08:00
FreeScout
9e0453e90a Improve text 2018-12-01 00:24:31 -08:00
FreeScout
88c317faba Redesign 2018-12-01 00:00:07 -08:00
FreeScout
5e8e37a22e Redesign 2018-11-30 23:30:20 -08:00
FreeScout
cb57f184b0 Redesign 2018-11-30 06:13:19 -08:00
FreeScout
6273c6e79d Redesign 2018-11-30 05:40:26 -08:00
FreeScout
a2a132eecc Hide tooltips on dropdown menu open 2018-11-30 03:29:24 -08:00
FreeScout
5b07fbeb3a Redesign 2018-11-30 03:27:34 -08:00
FreeScout
a57f621dfe Redesign 2018-11-30 01:45:04 -08:00
FreeScout
01d25b1a23 Redesign 2018-11-29 04:56:14 -08:00
FreeScout
1bd75719fe Merge branch 'master' of github.com:freescout-helpdesk/freescout 2018-11-27 22:10:43 -08:00
FreeScout
b37c39b853 Merge pull request #160 from Junker/conversations-mobile-select-longpress
Long press instead of checkboxes in mobile version
2018-11-28 09:10:19 +03:00
FreeScout
9bd0dda3dd Fix options caching 2018-11-27 22:05:13 -08:00
Junker
0885022b15 conversations table: fix attachment icon for mobile version 2018-11-27 10:03:04 +01:00
FreeScout
0c413261b4 Merge branch 'master' of github.com:freescout-helpdesk/freescout 2018-11-27 00:20:47 -08:00
FreeScout
b1f87e64e5 Options default values from config 2018-11-27 00:04:56 -08:00
Junker
0ee49fd9e2 Long press instead of checkboxes in mobile version 2018-11-26 20:55:34 +01:00
FreeScout
32662c4964 Merge pull request #159 from freescout-helpdesk/analysis-qMLB4v
Apply fixes from StyleCI
2018-11-26 15:48:40 +03:00
FreeScout
a59d52339e Apply fixes from StyleCI 2018-11-26 12:47:05 +00:00
FreeScout
447c40483c Fix conversations checkboxes on mobile version 2018-11-26 04:44:29 -08:00
FreeScout
6bc85bf399 Fix typo in FetchMonitor 2018-11-26 04:41:49 -08:00
FreeScout
de7da536bf Merge branch 'master' of github.com:freescout-helpdesk/freescout 2018-11-26 04:39:28 -08:00
FreeScout
d6ead86d46 Langauge option in web installer 2018-11-26 04:38:30 -08:00
FreeScout
0a34644369 Merge branch 'master' into translate 2018-11-26 04:30:22 -08:00
FreeScout
5120af7953 User language 2018-11-26 04:23:27 -08:00
FreeScout
67668a6227 User language 2018-11-26 02:55:37 -08:00
FreeScout
700f140a0e Send user notifications in user language 2018-11-26 02:36:07 -08:00
FreeScout
448db8f273 User language 2018-11-26 02:11:44 -08:00
FreeScout
549a36e7a2 Display custom new languages in Settings 2018-11-26 01:22:05 -08:00
FreeScout
2cf4f914e6 French translation 2018-11-26 01:00:18 -08:00
FreeScout
51d06959ba Importing translations 2018-11-26 00:59:10 -08:00
FreeScout
d4fcaf8a5c Download translations 2018-11-25 23:21:49 -08:00
FreeScout
599e45a479 Merge pull request #158 from dryabov/patch-1
Fix "Class 'App\Console\Commands\Exception' not found" error
2018-11-25 18:17:28 +03:00
FreeScout
a79fb0e74b Translate 2018-11-25 07:11:33 -08:00
FreeScout
286158b177 Importing and exporting translations 2018-11-25 03:18:53 -08:00
FreeScout
cd88e1aa67 Set timezone via Settings 2018-11-24 23:16:20 -08:00
FreeScout
57aec4e132 Check public/modules folder permissions 2018-11-24 08:48:53 -08:00
FreeScout
42eff0f61b Translate 2018-11-24 08:48:06 -08:00
Denis Ryabov
f39dd27fab Fix "Class 'App\Console\Commands\Exception' not found" error 2018-11-24 17:21:40 +03:00
FreeScout
aaabffb9aa Modules cards style 2018-11-24 05:11:20 -08:00
FreeScout
7ae110c866 Permissions and symlinks in System status 2018-11-24 04:08:32 -08:00
FreeScout
332b95965c Add vendor files to dist 2018-11-24 02:27:05 -08:00
FreeScout
463a9e059d Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2018-11-24 02:25:57 -08:00
FreeScout
7670ed52ce v1.0.7 2018-11-24 02:25:00 -08:00
FreeScout
dd36c6efb3 Merge pull request #150 from freescout-helpdesk/analysis-zOeVOl
Apply fixes from StyleCI
2018-11-24 13:12:40 +03:00
FreeScout
3e02679738 Apply fixes from StyleCI 2018-11-24 10:11:51 +00:00
FreeScout
bc16a732b4 Fix deleted drafts counter 2018-11-24 02:10:12 -08:00
FreeScout
2d32550714 Merge pull request #149 from Junker/action-threads
add history threads for restore/delete conversation action
2018-11-24 11:57:13 +03:00
FreeScout
de9f0e1be5 Add Fetching finished message to fetch command 2018-11-24 00:52:31 -08:00
Junker
00659e33fb fix thread state on delete conversation 2018-11-24 09:50:10 +01:00
FreeScout
aaba49adf7 Permissions and symlinks in System status 2018-11-24 00:43:28 -08:00
Junker
cad35f5dd7 add history threads for restore/delete conversation action 2018-11-24 09:00:20 +01:00
FreeScout
20cb166ce8 Fix send test email 2018-11-23 23:05:44 -08:00
FreeScout
9f6da13a57 Remove help scout reference from notification settings 2018-11-23 02:59:52 -08:00
FreeScout
d509613d60 Fix typo in mailbox connection settings 2018-11-23 02:56:07 -08:00
FreeScout
cfc7345a9a Check php extensions in modules 2018-11-23 01:06:46 -08:00
FreeScout
5d06587318 Helpder log function 2018-11-23 01:06:32 -08:00
FreeScout
6ae8ba4237 Optimize restore conversation js 2018-11-22 06:55:20 -08:00
FreeScout
1f4c2fef7e Merge pull request #147 from Junker/restore-conversation
Restore conversation
2018-11-22 17:50:58 +03:00
FreeScout
2ccdbc1aa9 Merge branch 'master' of github.com:freescout-helpdesk/freescout 2018-11-22 06:46:06 -08:00
FreeScout
df2fd5bb19 Helper functions 2018-11-22 06:45:50 -08:00
Junker
5ab12c9a23 fix redirect after restore conversation 2018-11-22 08:20:53 +01:00
Junker
d7ea0764e6 restore conversation 2018-11-22 07:43:54 +01:00
FreeScout
d05ef0115d Merge pull request #140 from Junker/fix-conversations-table-checkbox
fix: make conversations checkbox clickable for mobile version
2018-11-20 15:48:57 +03:00
Junker
91726bad33 fix: make conversations checkbox clickable for mobile version 2018-11-20 11:40:29 +01:00
FreeScout
89aac1833d Fallback to utf8 encoding from utf8mb4 for MySQL lower 5.5.3 2018-11-18 03:12:07 -08:00
FreeScout
423f1de5c0 Remove samle db data from env.example 2018-11-18 02:20:51 -08:00
FreeScout
a606ee1861 Hide Enrich Customer Data option 2018-11-18 01:39:07 -08:00
FreeScout
30e45c0558 Fix bool options in settings 2018-11-18 01:20:14 -08:00
FreeScout
70704c2bef Fix processing References header on fetching emails 2018-11-17 23:07:17 -08:00
FreeScout
8e359eda60 Add Yahoo reply separator 2018-11-17 22:45:48 -08:00
FreeScout
fdf1bea596 Note always changes status to Active and never changes Assignee 2018-11-17 22:33:07 -08:00
FreeScout
141635a2a7 Do not show module without aliases 2018-11-17 20:55:20 -08:00
FreeScout
a18c02bdec Improve mailboxes passwords encryption 2018-11-17 20:49:00 -08:00
FreeScout
99e6711b44 Add vendor files to dist 2018-11-17 06:06:07 -08:00
FreeScout
237c2e2c32 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2018-11-17 06:04:21 -08:00
FreeScout
7b0a1bfad4 v1.0.6 2018-11-17 06:01:30 -08:00
FreeScout
2fb503323b Fix he payload is invalid 2018-11-17 05:59:13 -08:00
FreeScout
0d9a332b89 Merge branch 'master' of github.com:freescout-helpdesk/freescout 2018-11-16 23:27:43 -08:00
FreeScout
474f186354 Conversation hooks 2018-11-16 23:21:46 -08:00
FreeScout
1ccc5d2a3f Merge pull request #135 from freescout-helpdesk/analysis-z9GlKN
Apply fixes from StyleCI
2018-11-17 08:04:41 +03:00
FreeScout
28861aec41 Apply fixes from StyleCI 2018-11-17 05:03:57 +00:00
FreeScout
030fc9e97d Fix bulk actions in search 2018-11-16 21:02:49 -08:00
FreeScout
b20e3e3bbb Merge pull request #129 from Junker/conversations-bulk-actions
Conversations bulk actions
2018-11-17 07:58:14 +03:00
FreeScout
0b7657268c Update link in user notification footer 2018-11-16 20:56:50 -08:00
FreeScout
ec2a3d116f Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2018-11-16 04:51:07 -08:00
FreeScout
d2f7e5c975 Remove gitter chat from readme 2018-11-16 04:50:27 -08:00
FreeScout
81f319dfea Move Fetch Emails button in Tools 2018-11-16 02:36:13 -08:00
FreeScout
c5de9bece3 Hide Sending Errors in Alerts Settings 2018-11-16 02:32:09 -08:00
FreeScout
c62a87af43 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2018-11-16 02:25:40 -08:00
FreeScout
b53cfe4b57 Do not save latest app version to file 2018-11-16 02:25:09 -08:00
FreeScout
fa902b3dec Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2018-11-16 01:41:43 -08:00
FreeScout
e3e7d281ef Add Migrate DB button to Tools 2018-11-16 01:38:00 -08:00
FreeScout
4e993a1ebb Smaller update now button in system status 2018-11-16 01:20:12 -08:00
FreeScout
913a521c77 Disable phpunit testing as dist vendor is cleared by removing unneeded files 2018-11-16 01:15:26 -08:00
FreeScout
fbe7c28b26 Add vendor files to dist 2018-11-16 01:03:16 -08:00
FreeScout
10e9172d9d Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2018-11-16 01:01:31 -08:00
FreeScout
8907232133 v.1.0.5 2018-11-16 01:00:53 -08:00
FreeScout
ee3151d085 Updating modules to the latest version 2018-11-16 00:57:37 -08:00
FreeScout
c39c1b4d75 Fix settings general template 2018-11-15 23:24:48 -08:00
FreeScout
46a36fe444 Add vendor files to dist 2018-11-15 23:16:39 -08:00
FreeScout
65eb9bda98 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2018-11-15 23:15:43 -08:00
FreeScout
0c64c7d08c Move mailbox name up in user notification 2018-11-15 22:35:13 -08:00
FreeScout
e5515c20b3 new_conversation_form.after in new conversation 2018-11-15 22:34:36 -08:00
FreeScout
4793a7666d Remove formatting button in editor 2018-11-15 21:40:58 -08:00
FreeScout
3e02ea636c Remove formattings button in editor 2018-11-15 21:19:06 -08:00
FreeScout
0dae0c9146 Update conversation button colors 2018-11-15 05:17:39 -08:00
FreeScout
060d24c95e Use marker in body in addition to Message ID to track replies 2018-11-15 05:03:53 -08:00
FreeScout
556660e6c1 Merge pull request #134 from freescout-helpdesk/analysis-z9GPe6
Apply fixes from StyleCI
2018-11-15 11:38:06 +03:00
FreeScout
c1be80b5d1 Apply fixes from StyleCI 2018-11-15 08:37:26 +00:00
FreeScout
255a8fd3c6 Merge composer.json 2018-11-15 00:32:32 -08:00
FreeScout
a29d832a56 Merge branch 'master' of github.com:freescout-helpdesk/freescout 2018-11-15 00:25:01 -08:00
Junker
c6cabafb20 moved conversation bulk actions code to ajax function 2018-11-15 08:13:14 +01:00
FreeScout
d2219b1673 Fix customer name in search 2018-11-14 22:35:41 -08:00
FreeScout
62d4a553d3 Styles and hooks 2018-11-14 22:35:25 -08:00
Junker
ff938c2239 Conversations bulk actions 2018-11-14 20:06:59 +01:00
FreeScout
2599e8293b Merge pull request #124 from guestisp/encrypt-mailbox-passwords
Crypt password used for email fetching
2018-11-14 12:17:11 +03:00
Alessandro Corbelli
6d8c5eeae0 Using doctrine/dbal for migrations 2018-11-14 09:49:18 +01:00
FreeScout
1c086a6c68 Enable debugbar provider autodiscovery 2018-11-14 00:01:27 -08:00
FreeScout
3e8e6f7095 Show link to logs in System status in case of fetch errors 2018-11-13 22:32:02 -08:00
FreeScout
a22e4538ab Override Laravel ProviderRepository.php to catch provider not found errors on updating 2018-11-13 22:27:07 -08:00
FreeScout
b323086c09 Add vendor files to dist 2018-11-13 22:08:27 -08:00
FreeScout
b09d957b12 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2018-11-13 22:06:15 -08:00
FreeScout
30ab811ba2 Merge branch 'master' of github.com:freescout-helpdesk/freescout 2018-11-13 22:03:47 -08:00
FreeScout
6aca1bea71 Override Laravel ProviderRepository.php to catch provider not found errors on updating 2018-11-13 22:03:34 -08:00
FreeScout
3287172473 Override Laravel ProviderRepository.php to catch provider not found errors on updating 2018-11-13 21:44:36 -08:00
FreeScout
d8acd413f4 Debugbar is enabled only if APP_ENV=local 2018-11-13 10:56:57 -08:00
FreeScout
06cdae9a87 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2018-11-13 09:32:29 -08:00
FreeScout
0e5e88d8ff Merge pull request #126 from guestisp/fix-125
Fix #125
2018-11-13 19:50:41 +03:00
Alessandro Corbelli
390fad48fb Fix #125 2018-11-13 15:45:29 +01:00
Alessandro Corbelli
5dec3b4e2d More styleci fixes 2018-11-13 15:06:56 +01:00
Alessandro Corbelli
340b277204 apply styleci fixes 2018-11-13 15:06:16 +01:00
Alessandro Corbelli
66930b7408 Crypt password used for email fetching 2018-11-13 15:04:26 +01:00
FreeScout
cfc59939f7 Email Template option hidden until somebody needs it 2018-11-13 05:53:28 -08:00
FreeScout
f2dedc0dfe Merge pull request #119 from guestisp/fix-spread_the_Word
Fix typo
2018-11-13 16:21:01 +03:00
FreeScout
5708909360 Settings hooks 2018-11-13 05:17:53 -08:00
Alessandro Corbelli
3f6a5fd2aa Fix typo 2018-11-13 12:44:44 +01:00
FreeScout
aa994eb2a5 Add PostgreSQL support 2018-11-12 23:15:48 -08:00
FreeScout
41bef2e90e Fix customers table index length 2018-11-12 22:46:38 -08:00
FreeScout
d1ba487fcd Check if .env.example exists in web installer 2018-11-12 22:45:05 -08:00
FreeScout
adab9fc7a8 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2018-11-12 06:04:51 -08:00
FreeScout
52942138f3 Enable open tracking by default 2018-11-12 06:04:07 -08:00
FreeScout
f16ec8422a Do not show last commit in update script 2018-11-12 05:49:17 -08:00
FreeScout
5b94ae8aa4 Add vendor files to dist 2018-11-12 05:42:30 -08:00
FreeScout
116c826262 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2018-11-12 05:40:12 -08:00
FreeScout
bf8460fc91 Merge branch 'master' of github.com:freescout-helpdesk/freescout 2018-11-12 05:38:32 -08:00
FreeScout
63156fb3b4 v1.0.4 2018-11-12 05:37:16 -08:00
FreeScout
d99636afb5 Merge pull request #116 from freescout-helpdesk/analysis-zGngEk
Apply fixes from StyleCI
2018-11-12 16:35:47 +03:00
FreeScout
c7745d424e Apply fixes from StyleCI 2018-11-12 13:35:06 +00:00
FreeScout
91bad70bdf Open tracking tuning 2018-11-12 05:23:19 -08:00
FreeScout
536a80d1c1 Merge pull request #105 from guestisp/email-open-tracking
Add tracking of opened emails
2018-11-12 15:53:02 +03:00
FreeScout
79957c1e58 Modules 2018-11-12 04:40:23 -08:00
Alessandro Corbelli
fae04cb1eb Fix Thread 2018-11-12 11:24:02 +01:00
Alessandro Corbelli
2a3df36798 Merge from master 2018-11-12 11:22:50 +01:00
Alessandro Corbelli
2ada8c787b Use tooltip for showing the full date of opening
Also fix #108
2018-11-12 11:18:48 +01:00
Alessandro Corbelli
80e1e47d1b Improvements on email open tracking 2018-11-12 11:09:36 +01:00
FreeScout
21412e8047 Merge pull request #114 from freescout-helpdesk/analysis-8bK04l
Apply fixes from StyleCI
2018-11-12 12:32:52 +03:00
FreeScout
6346437754 Apply fixes from StyleCI 2018-11-12 09:12:57 +00:00
FreeScout
a2e35bacbd Check passed parameters in \MailHelper::replaceMailVars() 2018-11-11 22:59:14 -08:00
FreeScout
074f98f375 Check php version in index.php and artisan 2018-11-11 21:50:43 -08:00
FreeScout
321987beb4 Styles updated 2018-11-11 21:26:40 -08:00
FreeScout
9a36e88451 New hooks 2018-11-11 21:26:26 -08:00
FreeScout
cd421c7afc Cache modules in memory 2018-11-11 21:25:45 -08:00
FreeScout
f1214bcaba Check PHP version in update script 2018-11-11 08:46:27 -08:00
FreeScout
f1b460be65 Add hint to Next Conversation field 2018-11-10 02:50:28 -08:00
FreeScout
93bc42bbc0 When creating conversation show number instead of ID 2018-11-10 02:45:35 -08:00
FreeScout
913f7d12f4 Merge pull request #102 from guestisp/add-next-ticket-feature
Allow to set the next conversation number
2018-11-10 13:43:21 +03:00
FreeScout
2db883812e Use version_compare in version check 2018-11-10 02:22:54 -08:00
Alessandro Corbelli
9a1bcac262 Add tracking of opened emails 2018-11-09 15:09:42 +01:00
Alessandro Corbelli
51a25ab804 - Option::remove('next_ticket') only if next_ticket is set
- Next Conversation # should display current value from the option or current max(number)+1
- Fix "if" by following Laravel standards
2018-11-09 11:35:38 +01:00
Alessandro Corbelli
80ae8f117f Allow to set the next conversation number 2018-11-09 10:28:08 +01:00
5222 changed files with 145719 additions and 437680 deletions

12
.editorconfig Normal file
View File

@@ -0,0 +1,12 @@
root = true
[*]
charset = utf-8
end_of_line = lf
indent_style = space
indent_size = 4
insert_final_newline = true
trim_trailing_whitespace = true
[*.json]
insert_final_newline = false

View File

@@ -9,9 +9,12 @@
# Application URL
APP_URL=https://example.com
# Use HTTP protocol and redirect to HTTPS
# Use HTTPS protocol and redirect to HTTPS
#APP_FORCE_HTTPS=true
# If you are using HTTPS, feel free to uncomment this line to improve security
#SESSION_SECURE_COOKIE=true
# Timezones: https://github.com/freescout-helpdesk/freescout/wiki/PHP-Timezones
# Comment it to use default timezone from php.ini
#APP_TIMEZONE=Europe/London
@@ -19,9 +22,10 @@ APP_URL=https://example.com
DB_CONNECTION=mysql
DB_HOST=localhost
DB_PORT=3306
DB_DATABASE=freescout
DB_USERNAME=freescout
DB_PASSWORD=XXX
DB_DATABASE=
DB_USERNAME=
# Maximum password length is 50 characters
DB_PASSWORD=
# Run the following console command to generate the key: php artisan key:generate
# Otherwise application will show the following error: "Whoops, looks like something went wrong"

3
.github/FUNDING.yml vendored Normal file
View File

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

View File

@@ -0,0 +1,12 @@
---
name: General Help Request
about: Create a general help request
---
* If you have "Whoops something went wrong" error see: https://github.com/freescout-helpdesk/freescout/wiki/Installation-Guide#11-troubleshooting
* 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
* Questions on issues with payments for modules should be sent by email: https://freescout.net/contact-us/
Still here? Well clean this out and go ahead :)

3
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View File

@@ -0,0 +1,3 @@
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
Now you can delete this text and type the description of your pull request...

5
.gitignore vendored
View File

@@ -27,6 +27,9 @@ Thumbs.db
/Modules
/Modules/**/.git
/public/modules
/public/docs
/storage/.ignore_locales
/storage/.installed
/tools
/tools
.well-known
/resources/lang/module.*

View File

@@ -1,4 +1,9 @@
# On some hostings it is impossible to change web root directory
# so we rewrite all web requests into /public folder
RewriteEngine on
RewriteRule (.*) /public/$1 [L]
<IfModule mod_rewrite.c>
RewriteEngine on
# On some hostings it does not work as "public/$1", but works as "/public/$1".
# But we can't write it as "/public/$1" because in this case it does not work
# when application is installed in subdirectory.
RewriteRule (.*) public/$1 [L]
</IfModule>

View File

@@ -3,10 +3,13 @@ language: php
php:
- 7.0
- 7.2
- 7.4
- 8.0
before_script:
- cp .env.travis .env
- mysql -e 'create database homestead_test;'
script:
- vendor/bin/phpunit
# phpunit testing is disabled as dist vendor is cleared by removing unneeded files
#script:
# - vendor/bin/phpunit

View File

@@ -5,70 +5,109 @@
<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.0%2B-blue.svg)](https://github.com/freescout-helpdesk/freescout#requirements) [![Laravel version](https://freescout-helpdesk.github.io/img/badges/Laravel-5.5-blue.svg)](https://github.com/freescout-helpdesk/freescout#requirements)
[![Build Status](https://travis-ci.org/freescout-helpdesk/freescout.svg?branch=dist)](https://freescout-helpdesk.github.io/img/badges/travis.svg) [![Join the chat at https://gitter.im/freescout-helpdesk/Lobby](https://badges.gitter.im/freescout-helpdesk/Lobby.svg)](https://gitter.im/freescout-helpdesk/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![HitCount](http://hits.dwyl.io/freescout-helpdesk/freescout.svg)](http://hits.dwyl.io/freescout-helpdesk/freescout)
[![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)
</div>
**FreeScout** is the super lightweight free open source help desk and shared inbox written in PHP7 (Laravel 5.5 framework) – self hosted clone of HelpScout. Now you can enjoy [free Zendesk & Help Scout](https://freescout.net) 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 materials to comply with GitHub's [DMCA Policy](https://help.github.com/articles/dmca-takedown-policy/) (trademark, logos, icons, CSS, JS, HTML files are copyrighted; idea, look and feel cannot be copyrighted).
**FreeScout** is the super lightweight free open source help desk and shared inbox written in PHP7+ (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](https://freescout-helpdesk.github.io/img/screenshots/screenshot.png)
## Table of Contents
* [Features](#features)
* [Demo](#demo)
* [Mobile Apps](#mobile-apps)
* [Requirements](#requirements)
* [News & Updates](#news--updates)
* [Installation](#installation)
* [Cloud Hosted](#cloud-hosted)
* [Modules](#modules)
* [Tools & Integrations](#tools--integrations)
* [News & Updates](#news--updates)
* [Contributing](#contributing)
* [Screenshots](#screenshots)
## Features
![FreeScout](https://raw.githubusercontent.com/freescout-helpdesk/freescout/master/public/img/banner.png)
* No limitations on the number of users, tickets, mailboxes, etc.
* 100% Mobile-friendly.
* Multilingual.
* Multilingual (English, Croatian, Danish, Dutch, French, German, Japanese, Italian, Persian, Polish, Portuguese, Russian, Spanish, Slovak, Swedish).
* Seamless email integration.
* 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.
* Notes.
* Internal notes.
* Pasting screenshots from the clipboard into the reply area.
* Dragging images into the reply area.
* Configuring notifications on a per user basis.
* Open tracking.
* Editing threads.
* Search.
* Edit/hide threads.
Need anything else? Suggest features [here](https://feedback.userreport.com/25a3cb5f-e4bd-4470-b6f3-79fcfaa8e90f/#ideas/popular)
Need anything else? Suggest features [here](https://freescout.net/request-feature/).
## Demo
**[Live Demo](https://demo.freescout.net)**
## 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>
## Requirements
FreeScout is a pure PHP/MySQL application, so it can be easily deployed even on a shared hosting.
* Nginx / Apache / IIS
* PHP 7.0+
* MySQL/MariaDB 5.0+
## News & Updates
If you would like to be notified about new releases, news and updates you can subscribe [here](https://feedburner.google.com/fb/a/mailverify?uri=freescout)
* PHP 7.1 - 8.1
* MySQL 5.0+ / MariaDB 5.0+ / PostgreSQL
## Installation
[Installation Guide](https://github.com/freescout-helpdesk/freescout/wiki/Installation-Guide)
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)
* [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)
## Cloud Hosted
[Cloud Hosted FreeScout](https://freescout.net/cloud-hosted/)
## Modules
FreeScout [Modules](https://github.com/freescout-helpdesk/freescout/wiki/FreeScout-Modules)
* [Official Modules](https://freescout.net/modules/)
* [Community Modules](https://freescout.net/community-modules/)
## Tools & Integrations
* [API](https://api-docs.freescout.net/)
* [Migrate to FreeScout](http://freescout.net/migrate/) (from any help desk)
* [Zapier](https://freescout.net/zapier/)
* [Integromat](https://freescout.net/integromat/)
## News & Updates
If you would like to be notified about important news and updates you can subscribe [here](https://freescout.net/subscribe/)
## 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/projects/1)
* [Todo list](https://github.com/freescout-helpdesk/freescout/labels/help%20wanted)
* [Translate](https://github.com/freescout-helpdesk/freescout/wiki/Translate)
* [Support project by leaving a feedback](https://github.com/freescout-helpdesk/freescout/wiki#support-project)
## Screenshots

5
SECURITY.md Normal file
View File

@@ -0,0 +1,5 @@
# Security Policy
## Reporting a Vulnerability
Email at support@freescout.net

View File

@@ -13,6 +13,15 @@ class ActivityLog extends Activity
const NAME_SYSTEM = 'system';
const NAME_APP_LOGS = 'app';
public static $available_logs = [
self::NAME_USER,
self::NAME_OUT_EMAILS,
self::NAME_EMAILS_SENDING,
self::NAME_EMAILS_FETCHING,
self::NAME_SYSTEM,
self::NAME_APP_LOGS,
];
const DESCRIPTION_USER_LOGIN = 'login';
const DESCRIPTION_USER_LOGOUT = 'logout';
const DESCRIPTION_USER_REGISTER = 'register';
@@ -87,7 +96,7 @@ class ActivityLog extends Activity
case self::NAME_APP_LOGS:
return __('App Logs');
default:
return ucfirst($log_name);
return ucwords(str_replace('_', ' ', $log_name));
}
}
@@ -95,6 +104,32 @@ class ActivityLog extends Activity
{
$col = str_replace('_', ' ', $col);
$col = ucfirst($col);
return $col;
}
/**
* Get log names.
*
* @return [type] [description]
*/
public static function getLogNames()
{
return self::select('log_name')->distinct()->pluck('log_name')->toArray();
}
/**
* Get available log names.
*
* @return [type] [description]
*/
public static function getAvailableLogs($check_existing = true)
{
$available_logs = self::$available_logs;
if ($check_existing) {
$available_logs = array_merge($available_logs, self::getLogNames());
}
return array_unique(\Eventy::filter('activity_log.available_logs', self::$available_logs));
}
}

View File

@@ -19,6 +19,8 @@ class Attachment extends Model
const DIRECTORY = 'attachment';
CONST DISK = 'private';
// https://github.com/Webklex/laravel-imap/blob/master/src/IMAP/Attachment.php
public static $types = [
'message' => self::TYPE_MESSAGE,
@@ -32,13 +34,8 @@ class Attachment extends Model
'other' => self::TYPE_OTHER,
];
/**
* Files with such extensions are being renamed on upload.
*/
public static $restricted_extensions = [
'php.*',
'sh',
'pl',
public static $type_extensions = [
self::TYPE_VIDEO => ['flv', 'mp4', 'm3u8', 'ts', '3gp', 'mov', 'avi', 'wmv']
];
public $timestamps = false;
@@ -60,30 +57,63 @@ class Attachment extends Model
return false;
}
// Check extension.
$extension = strtolower(pathinfo($file_name, PATHINFO_EXTENSION));
if (preg_match('/('.implode('|', self::$restricted_extensions).')/', $extension)) {
// Add underscore to the extension if file has restricted extension.
$file_name = $file_name.'_';
}
$orig_extension = pathinfo($file_name, PATHINFO_EXTENSION);
// Add underscore to the extension if file has restricted extension.
$file_name = \Helper::sanitizeUploadedFileName($file_name);
// Replace some symbols in file name.
// Gmail can not load image if it contains spaces.
$file_name = str_replace(' ', '-', $file_name);
$file_name = preg_replace('/[ #\/]/', '-', $file_name);
if (!$file_name) {
if (!$orig_extension) {
preg_match("/.*\/([^\/]+)$/", $mime_type, $m);
if (!empty($m[1])) {
$orig_extension = $m[1];
}
}
$file_name = uniqid();
if ($orig_extension) {
$file_name .= '.'.$orig_extension;
}
}
if (strlen($file_name) > 255) {
$without_ext = pathinfo($file_name, PATHINFO_FILENAME);
$extension = pathinfo($file_name, PATHINFO_EXTENSION);
// 125 because file name may have unicode symbols.
$file_name = \Helper::substrUnicode($without_ext, 0, 125-strlen($extension)-1);
$file_name .= '.'.$extension;
}
if (!$type) {
$type = self::detectType($mime_type);
$type = self::detectType($mime_type, $orig_extension);
}
$attachment = new self();
$attachment->thread_id = $thread_id;
$attachment->user_id = $user_id;
$attachment->user_id = $user_id;
$attachment->file_name = $file_name;
$attachment->mime_type = $mime_type;
$attachment->type = $type;
$attachment->embedded = $embedded;
$attachment->type = $type;
$attachment->embedded = $embedded;
$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->save();
return $attachment;
}
/**
* Save file to the disk and return file_dir.
*/
public static function saveFileToDisk($attachment, $file_name, $content, $uploaded_file)
{
// Save file from content or copy file.
// We have to keep file name as is, so if file exists we create extra folder.
// Examples: 1/2/3
@@ -93,26 +123,25 @@ class Attachment extends Model
do {
$i++;
$file_path = self::DIRECTORY.DIRECTORY_SEPARATOR.$file_dir.$i.DIRECTORY_SEPARATOR.$file_name;
} while (Storage::exists($file_path));
} while (Storage::disk(self::DISK)->exists($file_path));
$file_dir .= $i.DIRECTORY_SEPARATOR;
if ($uploaded_file) {
$uploaded_file->storeAs(self::DIRECTORY.DIRECTORY_SEPARATOR.$file_dir, $file_name);
$uploaded_file->storeAs(self::DIRECTORY.DIRECTORY_SEPARATOR.$file_dir, $file_name, ['disk' => self::DISK]);
} else {
Storage::put($file_path, $content);
Storage::disk(self::DISK)->put($file_path, $content);
}
$attachment->file_dir = $file_dir;
$attachment->size = Storage::size($file_path);
$attachment->save();
return $attachment;
return [
'file_dir' => $file_dir,
'file_path' => $file_path,
];
}
/**
* Get file path.
* Examples: 1/2, 1/3
* Examples: 1/2, 1/3.
*
* @param int $id
*
@@ -144,17 +173,23 @@ class Attachment extends Model
/**
* Detect attachment type by it's mime type.
*
* @param string $mime_type
*
* @param string $mime_type
*
* @return int
*/
public static function detectType($mime_type)
public static function detectType($mime_type, $extension = '')
{
if (preg_match("/^text\//", $mime_type)) {
return self::TYPE_TEXT;
} elseif (preg_match("/^message\//", $mime_type)) {
return self::TYPE_MESSAGE;
} elseif (preg_match("/^application\//", $mime_type)) {
// This is tricky mime type.
// For .mp4 mime type can be application/octet-stream
if (!empty($extension) && in_array(strtolower($extension), self::$type_extensions[self::TYPE_VIDEO])) {
return self::TYPE_VIDEO;
}
return self::TYPE_APPLICATION;
} elseif (preg_match("/^audio\//", $mime_type)) {
return self::TYPE_AUDIO;
@@ -188,7 +223,40 @@ class Attachment extends Model
*/
public function url()
{
return Storage::url($this->getStorageFilePath());
return Storage::url($this->getStorageFilePath()).'?id='.$this->id.'&token='.$this->getToken();
}
/**
* Get hashed security token for the attachment.
*/
public function getToken()
{
// \Hash::make() may contain . and / symbols which may cause problems.
return md5(config('app.key').$this->id.$this->size);
}
/**
* Outputs the current Attachment as download
*/
public function download($view = false)
{
$headers = [];
// #533
//return $this->getDisk()->download($this->getStorageFilePath(), \Str::ascii($this->file_name));
if ($view) {
$headers['Content-Disposition'] = '';
}
$file_name = $this->file_name;
if ($file_name == "RFC822"){
$file_name = $file_name.'.eml';
}
return $this->getDisk()->download($this->getStorageFilePath(), $file_name, $headers);
}
private function getDisk() {
return Storage::disk(self::DISK);
}
/**
@@ -201,14 +269,24 @@ class Attachment extends Model
return self::formatBytes($this->size);
}
/**
* attachment/...
*/
public function getStorageFilePath()
{
return self::DIRECTORY.DIRECTORY_SEPARATOR.$this->file_dir.$this->file_name;
}
public function getLocalFilePath()
/**
* /var/html/storage/app/attachment/...
*/
public function getLocalFilePath($full = true)
{
return Storage::path(self::DIRECTORY.DIRECTORY_SEPARATOR.$this->file_dir.$this->file_name);
if ($full) {
return $this->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;
}
}
public static function formatBytes($size, $precision = 0)
@@ -227,23 +305,105 @@ class Attachment extends Model
/**
* Delete attachments from disk and DB.
* Embeds are not taken into account.
*
* @param array $attachments
*
* @param array $attachments
*/
public static function deleteByIds($attachment_ids)
{
if (!count($attachment_ids)) {
return;
}
$attachments = Attachment::whereIn('id', $attachment_ids)->get();
$attachments = self::whereIn('id', $attachment_ids)->get();
// Delete from disk
self::deleteForever($attachments);
}
/**
* Delete attachments by thread IDs.
*/
public static function deleteByThreadIds($thread_ids)
{
if (!count($thread_ids)) {
return;
}
$attachments = self::whereIn('thread_id', $thread_ids)->get();
// Delete from disk
self::deleteForever($attachments);
}
public static function deleteForever($attachments)
{
// Delete from disk
foreach ($attachments as $attachment) {
echo $attachment->getStorageFilePath().' ';
Storage::delete($attachment->getStorageFilePath());
$attachment->getDisk()->delete($attachment->getStorageFilePath());
}
// Delete from DB
Attachment::whereIn('id', $attachment_ids)->delete();
self::whereIn('id', $attachments->pluck('id')->toArray())->delete();
}
/**
* Delete attachments and update Thread & Conversation.
*/
public static function deleteAttachments($attachments)
{
if (!$attachments instanceof \Illuminate\Support\Collection) {
$attachments = collect($attachments);
}
foreach ($attachments as $attachment) {
if ($attachment->thread_id && $attachment->thread
&& count($attachment->thread->attachments) <= 1
) {
$attachment->thread->has_attachments = false;
$attachment->thread->save();
// Update conversation.
$conversation = $attachment->thread->conversation;
foreach ($conversation->threads as $thread) {
if ($thread->has_attachments) {
break 2;
}
}
$conversation->has_attachments = false;
$conversation->save();
}
}
Attachment::deleteForever($attachments);
}
/**
* Create a copy of the attachment and it's file.
*/
public function duplicate($thread_id)
{
$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
);
$file_info = Attachment::saveFileToDisk($new_attachment, $new_attachment->file_name, '', $attachment_file);
if (!empty($file_info['file_dir'])) {
$new_attachment->file_dir = $file_info['file_dir'];
$new_attachment->save();
}
} catch (\Exception $e) {
\Helper::logException($e);
}
return $new_attachment;
}
public function getFileContents()
{
return $this->getDisk()->get($this->getStorageFilePath());
}
}

View File

@@ -3,16 +3,13 @@
* Created by PhpStorm.
* User: leemason
* Date: 31/10/15
* Time: 00:20
* Time: 00:20.
*/
namespace App\Broadcasting\Broadcasters;
use Carbon\Carbon;
use Illuminate\Broadcasting\Broadcasters\Broadcaster;
use Illuminate\Foundation\Application;
use Illuminate\Database\DatabaseManager;
use Illuminate\Support\Str;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
@@ -43,21 +40,22 @@ class PolycastBroadcaster extends Broadcaster
// insert the new event
\DB::table('polycast_events')->insert([
'channels' => json_encode($channels),
'event' => $event,
'payload' => json_encode($payload),
'created_at' => Carbon::now()->toDateTimeString()
'channels' => json_encode($channels),
'event' => $event,
'payload' => json_encode($payload),
'created_at' => Carbon::now()->toDateTimeString(),
]);
}
/**
* Authenticate the incoming request for a given channel.
*
* @param \Illuminate\Http\Request $request
* @param \Illuminate\Http\Request $request
*
* @return mixed
*/
public function auth($request) {
public function auth($request)
{
// For connect request
if (empty($request->channels)) {
return true;
@@ -67,19 +65,20 @@ class PolycastBroadcaster extends Broadcaster
foreach ($request->channels as $channel_name => $channel_info) {
// Copied from Illuminate\Broadcasting\Broadcasters\PusherBroadcaster
if (Str::startsWith($channel_name, ['private-', 'presence-']) &&
! $request->user()) {
echo 1;
exit();
throw new AccessDeniedHttpException;
!$request->user()) {
throw new AccessDeniedHttpException();
}
$channelName = Str::startsWith($channel_name, 'private-')
? Str::replaceFirst('private-', '', $channel_name)
: Str::replaceFirst('presence-', '', $channel_name);
// This throws an exception
parent::verifyUserCanAccessChannel(
$request, $channelName
);
if (Str::startsWith($channel_name, ['private-', 'presence-'])) {
$channelName = Str::startsWith($channel_name, 'private-')
? Str::replaceFirst('private-', '', $channel_name)
: Str::replaceFirst('presence-', '', $channel_name);
// This throws an exception if needed.
parent::verifyUserCanAccessChannel(
$request, $channelName
);
}
}
return true;
@@ -88,11 +87,13 @@ class PolycastBroadcaster extends Broadcaster
/**
* Return the valid authentication response.
*
* @param \Illuminate\Http\Request $request
* @param mixed $result
* @param \Illuminate\Http\Request $request
* @param mixed $result
*
* @return mixed
*/
public function validAuthenticationResponse($request, $result) {
public function validAuthenticationResponse($request, $result)
{
// By some reason this is never called
return false;
@@ -107,20 +108,20 @@ class PolycastBroadcaster extends Broadcaster
// ]]);
}
public function isDeferred()
{
public function isDeferred()
{
return false;
}
}
/**
* Created as there was an error:
* "Call to undefined method App\Broadcasting\Broadcasters\PolycastBroadcaster::channel()"
*
* It is called from routes/channels.php
*/
/*
* Created as there was an error:
* "Call to undefined method App\Broadcasting\Broadcasters\PolycastBroadcaster::channel()"
*
* It is called from routes/channels.php
*/
// public function channel($channel, callable $callback)
// {
// return true;
// //return (int) $user->id === (int) $id;
// }
}
}

View File

@@ -3,19 +3,17 @@
namespace App\Channels;
use App\Events\RealtimeBroadcastNotificationCreated;
use RuntimeException;
use Illuminate\Notifications\Notification;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Notifications\Channels\BroadcastChannel;
use Illuminate\Notifications\Messages\BroadcastMessage;
use Illuminate\Notifications\Notification;
class RealtimeBroadcastChannel extends BroadcastChannel
{
/**
* Send the given notification immediately using non-quable event.
*
* @param mixed $notifiable
* @param \Illuminate\Notifications\Notification $notification
* @param mixed $notifiable
* @param \Illuminate\Notifications\Notification $notification
*
* @return array|null
*/
public function send($notifiable, Notification $notification)

View File

@@ -0,0 +1,117 @@
<?php
namespace App\Console\Commands;
use Carbon\Carbon;
use Illuminate\Console\Command;
class CheckConvViewers extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'freescout:check-conv-viewers';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Check if user finished viewing conversation';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
// Check dates in cache.
$cache_key = 'conv_view';
$cache_data = \Cache::get($cache_key);
if (empty($cache_data) || !is_array($cache_data)) {
return;
}
$now = Carbon::now();
$need_update = false;
foreach ($cache_data as $conversation_id => $conv_data) {
if (empty($conv_data) || !is_array($conv_data)) {
continue;
}
foreach ($conv_data as $user_id => $data) {
if (!isset($data['t']) || !isset($data['r'])) {
continue;
}
$view_date = Carbon::createFromFormat('Y-m-d H:i:s', $data['t']);
if ($view_date && $now->diffInSeconds($view_date) > 25) {
// Remove user from viewers.
unset($cache_data[$conversation_id][$user_id]);
if (empty($cache_data[$conversation_id])) {
unset($cache_data[$conversation_id]);
}
$need_update = true;
\Cache::forget('conv_view_'.$user_id.'_'.$conversation_id);
// Create event to let other users know that user finished viewing conversation.
$notification_data = [
'conversation_id' => $conversation_id,
'user_id' => $user_id,
];
event(new \App\Events\RealtimeConvViewFinish($notification_data));
\Eventy::action('conversation.view.finish', $conversation_id, $user_id, $now->diffInSeconds($view_date));
}
}
}
if ($need_update) {
// Update conversation cache data.
\Cache::put($cache_key, $cache_data, 20 /*minutes*/);
}
/*$cache_key = 'conv_view_'.$this->user_id.'_'.$this->conversation_id;
$cache_data = \Cache::get($cache_key);
if (!isset($cache_data['t']) || !isset($cache_data['r'])) {
return;
}
$view_date = Carbon::createFromFormat('Y-m-d H:i:s', $cache_data['t']);
$now = Carbon::now();
if ($view_date && $now->diffInSeconds($view_date) > 30) {
$cache_key = 'conv_view';
if (!empty($cache_data[$this->conversation_id]) && !empty($cache_data[$this->conversation_id][$this->user_id])) {
// Remove user from viewers.
unset($cache_data[$this->conversation_id][$this->user_id]);
// Update conversation cache data.
\Cache::put($cache_key, $cache_data, 1);
}
// Create event to let other users know that user finished viewing conversation.
$notification_data = [
'conversation_id' => $conversation->id,
'user_id' => $user->id,
];
event(new \App\Events\RealtimeConvViewFinish($notification_data));
}*/
}
}

View File

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

View File

@@ -11,7 +11,7 @@ class ClearCache extends Command
*
* @var string
*/
protected $signature = 'freescout:clear-cache {--doNotCacheConfig}';
protected $signature = 'freescout:clear-cache {--doNotCacheConfig} {--doNotGenerateVars}';
/**
* The console command description.
@@ -44,6 +44,15 @@ class ClearCache extends Command
$this->call('config:clear');
} else {
$this->call('config:cache');
// Laravel users `require` function to include config.php
// If opcache is being used for few seconds config.php is being cached.
if (function_exists('opcache_invalidate')) {
opcache_invalidate(app()->getCachedConfigPath());
}
}
// Regenerate vars to get new data from .env
if (!$this->option('doNotGenerateVars')) {
$this->call('freescout:generate-vars');
}
}
}

View File

@@ -12,7 +12,7 @@ class CreateUser extends Command
*
* @var string
*/
protected $signature = 'freescout:create-user';
protected $signature = 'freescout:create-user {--role=} {--firstName=} {--lastName=} {--email=} {--password=}';
/**
* The console command description.
@@ -46,40 +46,57 @@ class CreateUser extends Command
).'.model'
);
$user = new $class();
$fillables = ['role', 'first_name', 'last_name', 'email', 'password'];
foreach ($fillables as $key => $fillable) {
if ($fillable == 'password') {
$user->$fillable = \Hash::make($this->secret(($key + 1).'/'.count($fillables)." User $fillable"));
} elseif ($fillable == 'role') {
$user->$fillable = $this->ask(($key + 1).'/'.count($fillables)." User $fillable (admin/user)", 'admin');
if (!$user->$fillable) {
$user->$fillable = 'admin';
}
while (!in_array($user->$fillable, User::$roles)) {
$this->error('Incorrect role');
$user->$fillable = $this->ask('Please enter valid role');
}
$user->$fillable = array_flip(User::$roles)[$user->$fillable];
} else {
$user->$fillable = $this->ask(($key + 1).'/'.count($fillables)." User $fillable");
if ($fillable == 'email') {
while (!filter_var($user->$fillable, FILTER_VALIDATE_EMAIL)) {
$this->error('Incorrect email address');
$user->$fillable = $this->ask('Please enter valid email address');
}
}
$user->role = $this->option('role');
if ($user->role ) {
if (!in_array($user->role , User::$roles)) {
$this->error('Invalid role');
return false;
}
} else {
$user->role = $this->ask('User role (admin/user)', 'admin');
while (!in_array($user->role, User::$roles)) {
$this->error('Invalid role');
$user->role = $this->ask('Please enter valid role');
}
}
$user->role = array_flip(User::$roles)[$user->role];
$user->first_name = $this->option('firstName') ? $this->option('firstName') : $this->ask('User first name');
$user->last_name = $this->option('lastName') ? $this->option('lastName') : $this->ask('User last name');
$user->email = $this->option('email');
if ($user->email) {
if (!filter_var($user->email, FILTER_VALIDATE_EMAIL)) {
$this->error('Invalid email address');
return false;
}
} else {
$user->email = $this->ask('User email address');
while (!filter_var($user->email, FILTER_VALIDATE_EMAIL)) {
$this->error('Incorrect email address');
$user->email = $this->ask('Please enter valid email address');
}
}
$user->password = \Hash::make($this->option('password') ? $this->option('password') : $this->secret('User password'));
if ($this->confirm('Do you want to create the user?', true)) {
if ($user->isAdmin()) {
$user->invite_state = User::INVITE_STATE_ACTIVATED;
}
$user->save();
$this->info("User created (id: {$user->id})");
try {
$user->save();
} catch (\Exception $e) {
$this->line($e->getMessage());
$this->error('User already exists.');
return false;
}
}
$this->info('User created with id: '.$user->id);
return true;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -39,12 +39,16 @@ class FetchMonitor extends Command
{
$now = time();
$last_successful_run = \Option::get('fetch_emails_last_successful_run');
if ($last_successful_run && $last_successful_run < $now - \Option::get('alert_fetch_period')*60) {
$options = \Option::getOptions([
'alert_fetch_period',
'fetch_emails_last_successful_run',
]);
$last_successful_run = $options['fetch_emails_last_successful_run'];
if ($last_successful_run && $last_successful_run < $now - ((config('app.fetch_schedule') * 60) + ($options['alert_fetch_period'] * 60))) {
$mins_ago = floor(($now - $last_successful_run) / 60);
$text = 'There are some problems fetching emails: last time emails were successfully fetched <strong>'.$mins_ago.' minutes ago</strong>. Please check <a href="'.route('logs', ['name' => 'fetch_errors']).'">fetching logs</a> and <a href="'.route('system').'#cron">make sure</a> that the flollowing cron task is running: <code>php artisan schedule:run</code>';
$text = 'There are some problems fetching emails: last time emails were successfully fetched <strong>'.$mins_ago.' minutes ago</strong>. Please check <a href="'.route('logs', ['name' => 'fetch_errors']).'">fetching logs</a> and <a href="'.route('system').'#cron">make sure</a> that the following cron task is running: <code>php artisan schedule:run</code>';
if (\Option::get('alert_fetch') && !\Option::get('alert_fetch_sent')) {
// We send alert only once

View File

@@ -43,18 +43,33 @@ class GenerateVars extends Command
{
try {
$params = [
'locales' => config('app.locales'),
'locales' => \Helper::getAllLocales(),
];
$filesystem = new Filesystem();
//$filesystem = new Filesystem();
$file_path = public_path('js/vars.js');
//$file_path = public_path('js/vars.js');
$file_path = storage_path('app/public/js/vars.js');
$compiled = view('js/vars', $params)->render();
$content = view('js/vars', $params)->render();
$filesystem->put($file_path, $compiled);
$this->info("Created: {$file_path}");
//$filesystem->put($file_path, $content);
// Save vars only if content changed
try {
if (\Storage::exists('js/vars.js')) {
$old_content = \Storage::get('js/vars.js');
if ($content != $old_content) {
\Storage::put('js/vars.js', $content);
}
} else {
\Storage::put('js/vars.js', $content);
}
$this->info("Created: ".substr($file_path, strlen(base_path())+1));
} catch (\Exception $e) {
$msg = "Error occured saving /storage/app/public/js/vars.js. ".\Helper::formatException($e);
\Log::error($msg);
$this->error($msg);
}
} catch (\Exception $e) {
$this->error($e->getMessage());
}

View File

@@ -0,0 +1,61 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
class LogoutUsers extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'freescout:logout-users';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Logout all users';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
try {
// Remove files from storage/frameworks/sessions
$files = \File::files(storage_path('framework/sessions'));
$count = 0;
foreach ($files as $file) {
try {
$deleted = \File::delete($file->getPathname());
if ($deleted) {
$count++;
}
} catch (\Exception $e) {
$this->error($e->getMessage());
}
}
$this->line('Deleted sessions: '.$count);
} catch (\Exception $e) {
$this->error($e->getMessage());
}
}
}

View File

@@ -0,0 +1,96 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
class LogsMonitor extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'freescout:logs-monitor';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Send new log records by email';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$now = \Carbon\Carbon::now();
$options = \Option::getOptions([
'alert_logs_names',
'alert_logs_period',
], [
'alert_logs_period' => config('app.alert_logs_period'),
]);
if (!$options['alert_logs_names']) {
$this->error('['.date('Y-m-d H:i:s').'] No logs to monitor selected');
return;
}
if (!$options['alert_logs_period']) {
$this->error('['.date('Y-m-d H:i:s').'] No logs monitoring period set');
return;
}
$logs = \App\ActivityLog::whereIn('log_name', $options['alert_logs_names'])
->where('created_at', '>=', \Carbon\Carbon::now()->modify('-1 '.$options['alert_logs_period'])->toDateTimeString())
->where('created_at', '<', $now->toDateTimeString())
->get();
if (!count($logs)) {
$this->line('['.date('Y-m-d H:i:s').'] No new log records found for the last '.$options['alert_logs_period']);
return;
}
$names = $logs->pluck('log_name')->unique()->toArray();
$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 .= '</li>';
}
$text .= '</ul>';
foreach ($names as $name) {
$text .= '<br/><strong>'.\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/>';
}
}
// Send alert.
\MailHelper::sendAlertMail($text, 'Logs Monitoring');
$this->line($text);
$this->info('['.date('Y-m-d H:i:s').'] Monitoring finished');
}
}

View File

@@ -52,6 +52,7 @@ class ModuleBuild extends Command
}
if (!$modules_aliases) {
$this->error('No modules found');
return;
}
$all = true;
@@ -70,6 +71,7 @@ class ModuleBuild extends Command
$module = \Module::findByAlias($module_alias);
if (!$module) {
$this->error('Module with the specified alias not found: '.$module_alias);
return;
}
$this->buildModule($module);
@@ -84,6 +86,7 @@ class ModuleBuild extends Command
$public_symlink = public_path('modules').DIRECTORY_SEPARATOR.$module->alias;
if (!file_exists($public_symlink)) {
$this->error('Public symlink ['.$public_symlink.'] not found. Run module installation command first: php artisan freescout:module-install');
return;
}
@@ -108,7 +111,6 @@ class ModuleBuild extends Command
}
$this->info("Created: {$file_path}");
} catch (\Exception $e) {
$this->error($e->getMessage());
}

View File

@@ -0,0 +1,102 @@
<?php
namespace App\Console\Commands;
use App\Misc\WpApi;
use Illuminate\Console\Command;
class ModuleCheckLicenses extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'freescout:module-check-licenses';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Check licenses for modules';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
// Get active official modules and check validity of their licenses
$modules = \Module::getActive();
$this->info('Active modules found: '.count($modules));
$params = [
'url' => \App\Module::getAppUrl(),
'data' => [],
];
foreach ($modules as $module) {
$license = $module->getLicense();
if (!$module->isOfficial() || !$license) {
continue;
}
$data['license'] = $license;
$data['module_alias'] = $module->getAlias();
$params['data'][] = $data;
}
$result = WpApi::checkLicenses($params);
if (!empty($result['statuses'])) {
foreach ($modules as $module) {
$module_alias = $module->getAlias();
foreach ($result['statuses'] as $result_module_alias => $status) {
if ($result_module_alias != $module_alias) {
continue;
}
if (!empty($status) && $status != 'valid') {
$msg = 'Module '.$module->getName().' has been deactivated due to invalid license: '.$status;
$this->error($module->getName().': '.$msg);
// Deactive module
\App\Module::deactiveModule($module->getAlias(), true);
// Inform admin
\Log::error($msg);
activity()
->withProperties([
'error' => $msg,
])
->useLog(\App\ActivityLog::NAME_SYSTEM)
->log(\App\ActivityLog::DESCRIPTION_SYSTEM_ERROR);
} else {
$this->info($module->getName().': OK');
}
continue 2;
}
$this->info($module->getName().': Unknown status');
}
}
$this->info('Checking licenses finished');
}
}

View File

@@ -1,7 +1,8 @@
<?php
/**
* php artisan freescout:module-install modulealias
* php artisan freescout:module-install modulealias.
*/
namespace App\Console\Commands;
use Illuminate\Console\Command;
@@ -56,6 +57,7 @@ class ModuleInstall extends Command
}
if (!$modules_aliases) {
$this->error('No modules found');
return;
}
$install_all = $this->confirm('You have not specified a module alias, would you like to install all available modules ('.implode(', ', $modules_aliases).')?');
@@ -74,6 +76,7 @@ class ModuleInstall extends Command
$module = \Module::findByAlias($module_alias);
if (!$module) {
$this->error('Module with the specified alias not found: '.$module_alias);
return;
}
$this->call('module:migrate', ['module' => $module->getName(), '--force' => true]);
@@ -91,7 +94,7 @@ class ModuleInstall extends Command
if (file_exists($from)) {
return $this->info('Public symlink already exists');
}
try {
symlink($to, $from);
} catch (\Exception $e) {

View File

@@ -2,9 +2,8 @@
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Filesystem\Filesystem;
use Axn\Laroute\Routes\Collection as Routes;
use Illuminate\Console\Command;
class ModuleLaroute extends Command
{
@@ -31,8 +30,8 @@ class ModuleLaroute extends Command
{
$app = app();
$this->config = $app['config'];
$this->generator = $app->make('Lord\Laroute\Generators\GeneratorInterface');
$this->config = $app['config'];
$this->generator = $app->make('Lord\Laroute\Generators\GeneratorInterface');
parent::__construct();
}
@@ -58,6 +57,7 @@ class ModuleLaroute extends Command
}
if (!$modules_aliases) {
$this->error('No modules found');
return;
}
$all = true;
@@ -75,6 +75,7 @@ class ModuleLaroute extends Command
$module = \Module::findByAlias($module_alias);
if (!$module) {
$this->error('Module with the specified alias not found: '.$module_alias);
return;
}
$this->generateModuleRoutes($module);
@@ -88,6 +89,7 @@ class ModuleLaroute extends Command
$public_symlink = public_path('modules').DIRECTORY_SEPARATOR.$module->getAlias();
if (!file_exists($public_symlink)) {
$this->error('Public symlink ['.$public_symlink.'] not found. Run module installation command first: php artisan freescout:module-install');
return;
}
@@ -123,16 +125,15 @@ class ModuleLaroute extends Command
*/
protected function getTemplateData()
{
$namespace = $this->getOptionOrConfig('namespace');
$routes = $this->routes->toJSON();
$absolute = $this->config->get('laroute.absolute', false);
$rootUrl = $this->config->get('app.url', '');
$prefix = $this->config->get('laroute.prefix', '');
$namespace = $this->getOptionOrConfig('namespace');
$routes = $this->routes->toJSON();
$absolute = $this->config->get('laroute.absolute', false);
$rootUrl = $this->config->get('app.url', '');
$prefix = $this->config->get('laroute.prefix', '');
return compact('namespace', 'routes', 'absolute', 'rootUrl', 'prefix');
}
/**
* Get the path where the file will be generated.
*
@@ -140,7 +141,7 @@ class ModuleLaroute extends Command
*/
protected function getFileGenerationPath($module_alias)
{
$path = 'public/modules/'.$module_alias.'/js';
$path = 'public/modules/'.$module_alias.'/js';
$filename = 'laroute'; //$this->getOptionOrConfig('filename');
return "{$path}/{$filename}.js";

View File

@@ -43,11 +43,12 @@ class Update extends Command
if (!$this->confirmToProceed()) {
return;
}
@ini_set('memory_limit', '128M');
if (\Updater::isNewVersionAvailable(config('app.version'))) {
$this->info('Updating... This may take several minutes');
try {
// Script may fail here and stop with the error:
// PHP Fatal error: Allowed memory size of 94371840 bytes exhausted

View File

@@ -0,0 +1,46 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
class UpdateFolderCounters extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'freescout:update-folder-counters';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Update counters for all folders';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
foreach (\App\Folder::get() as $folder) {
$folder->updateCounters();
$this->line('Updated counters for folder: '.$folder->id);
}
$this->info('Updating finished');
}
}

View File

@@ -5,6 +5,8 @@ namespace App\Console;
use Carbon\Carbon;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
use App\Misc\Mail;
use App\Option;
class Kernel extends ConsoleKernel
{
@@ -27,9 +29,11 @@ class Kernel extends ConsoleKernel
*/
protected function schedule(Schedule $schedule)
{
// Keep in mind that this function is also called on clearing cache.
// Remove failed jobs
$schedule->command('queue:flush')
->daily();
->weekly();
// Restart processing queued jobs (just in case)
$schedule->command('queue:restart')
@@ -39,43 +43,138 @@ class Kernel extends ConsoleKernel
->everyMinute()
->withoutOverlapping();
// Fetch emails from mailboxes
$schedule->command('freescout:fetch-emails')
$schedule->command('freescout:update-folder-counters')
->hourly();
$crc = crc32(config('app.key'));
$schedule->command('freescout:module-check-licenses')
->cron((int)($crc % 59).' '.(int)($crc % 23).' * * *');
// Check if user finished viewing conversation.
$schedule->command('freescout:check-conv-viewers')
->everyMinute()
->withoutOverlapping();
$schedule->command('freescout:clean-send-log')
->monthly();
// Logs monitoring.
$alert_logs_period = config('app.alert_logs_period');
if (config('app.alert_logs') && $alert_logs_period) {
$logs_cron = '';
switch ($alert_logs_period) {
case 'hour':
$logs_cron = '0 * * * *';
break;
case 'day':
$logs_cron = '0 0 * * *';
break;
case 'week':
$logs_cron = '0 0 * * 0';
break;
case 'month':
$logs_cron = '0 0 1 * *';
break;
}
if ($logs_cron) {
$schedule->command('freescout:logs-monitor')
->cron($logs_cron)
->withoutOverlapping();
}
}
// Fetch emails from mailboxes
$fetch_command = $schedule->command('freescout:fetch-emails')
->withoutOverlapping()
->sendOutputTo(storage_path().'/logs/fetch-emails.log');
switch (config('app.fetch_schedule')) {
case Mail::FETCH_SCHEDULE_EVERY_FIVE_MINUTES:
$fetch_command->everyFiveMinutes();
break;
case Mail::FETCH_SCHEDULE_EVERY_TEN_MINUTES:
$fetch_command->everyTenMinutes();
break;
case Mail::FETCH_SCHEDULE_EVERY_FIFTEEN_MINUTES:
$fetch_command->everyFifteenMinutes();
break;
case Mail::FETCH_SCHEDULE_EVERY_THIRTY_MINUTES:
$fetch_command->everyThirtyMinutes();
break;
case Mail::FETCH_SCHEDULE_HOURLY:
$fetch_command->Hourly();
break;
default:
$fetch_command->everyMinute();
break;
}
$schedule = \Eventy::filter('schedule', $schedule);
// Command runs as subprocess and sets cache mutex. If schedule:run command is killed
// subprocess does not clear the mutex and it stays in the cache until cache:clear is executed.
// By default, the lock will expire after 24 hours.
//
// cache:clear clears the mutex, but sometimes process continues running, so we need to kill it.
if (function_exists('shell_exec')) {
$running_commands = 0;
try {
$processes = preg_split("/[\r\n]/", shell_exec("ps aux | grep 'queue:work'"));
foreach ($processes as $process) {
preg_match("/^[\S]+\s+([\d]+)\s+/", $process, $m);
if (!preg_match("/(sh \-c|grep )/", $process) && !empty($m[1])) {
$running_commands++;
}
}
} catch (\Exception $e) {
// Do nothing
}
if ($running_commands > 1) {
$running_commands = $this->getRunningQueueProcesses();
if (count($running_commands) > 1) {
// Stop all queue:work processes.
// queue:work command is stopped by settings a cache key
\Cache::forever('illuminate:queue:restart', Carbon::now()->getTimestamp());
\Helper::queueWorkRestart();
// Sometimes processes stuck and just continue running, so we need to kill them.
// Sleep to let processes stop.
sleep(1);
// Check processes again.
$worker_pids = $this->getRunningQueueProcesses();
if (count($worker_pids) > 1) {
// Current process also has to be killed, as otherwise it "stucks"
// $current_pid = getmypid();
// foreach ($worker_pids as $i => $pid) {
// if ($pid == $current_pid) {
// unset($worker_pids[$i]);
// break;
// }
// }
shell_exec('kill '.implode(' | kill ', $worker_pids));
}
}
}
$schedule->command('queue:work', Config('app.queue_work_params'))
$queue_work_params = Config('app.queue_work_params');
// Add identifier to avoid conflicts with other FreeScout instances on the same server.
$queue_work_params['--queue'] .= ','.\Helper::getWorkerIdentifier();
$schedule->command('queue:work', $queue_work_params)
->everyMinute()
->withoutOverlapping()
->sendOutputTo(storage_path().'/logs/queue-jobs.log');
}
/**
* Get pids of the queue:work processes.
*
* @return [type] [description]
*/
protected function getRunningQueueProcesses()
{
$pids = [];
try {
$processes = preg_split("/[\r\n]/", shell_exec("ps aux | grep '".\Helper::getWorkerIdentifier()."'"));
foreach ($processes as $process) {
preg_match("/^[\S]+\s+([\d]+)\s+/", $process, $m);
if (!preg_match("/(sh \-c|grep )/", $process) && !empty($m[1])) {
$pids[] = $m[1];
}
}
} catch (\Exception $e) {
// Do nothing
}
return $pids;
}
/**
* Register the commands for the application.
*
@@ -85,6 +184,13 @@ class Kernel extends ConsoleKernel
{
$this->load(__DIR__.'/Commands');
// Swiftmailer uses $_SERVER['SERVER_NAME'] in transport_deps.php
// to set the host for EHLO command, if it is empty it uses [127.0.0.1].
// G Suite sometimes rejects emails with EHLO [127.0.0.1].
if (empty($_SERVER['SERVER_NAME'])) {
$_SERVER['SERVER_NAME'] = parse_url(config('app.url'), PHP_URL_HOST);
}
require base_path('routes/console.php');
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -3,11 +3,10 @@
namespace App;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Input;
class ConversationFolder extends Model
{
/**
/**
* The table associated with the model.
*
* @var string
@@ -15,4 +14,4 @@ class ConversationFolder extends Model
protected $table = 'conversation_folder';
protected $fillable = ['folder_id', 'conversation_id'];
}
}

View File

@@ -4,14 +4,20 @@ namespace App;
use App\Email;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Storage;
use Watson\Rememberable\Rememberable;
class Customer extends Model
{
use Rememberable;
// This is obligatory.
public $rememberCacheDriver = 'array';
const PHOTO_DIRECTORY = 'customers';
const PHOTO_SIZE = 64; // px
const PHOTO_QUALITY = 77;
/**
* Genders.
*/
@@ -46,7 +52,7 @@ class Customer extends Model
self::PHONE_TYPE_HOME => 'home',
self::PHONE_TYPE_OTHER => 'other',
self::PHONE_TYPE_MOBILE => 'mobile',
self::PHONE_TYPE_FAX => 'Fax',
self::PHONE_TYPE_FAX => 'fax',
self::PHONE_TYPE_PAGER => 'pager',
];
@@ -79,51 +85,52 @@ class Customer extends Model
/**
* Chat types.
*/
const CHAT_TYPE_AIM = 1;
const CHAT_TYPE_GTALK = 2;
const CHAT_TYPE_ICQ = 3;
const CHAT_TYPE_XMPP = 4;
const CHAT_TYPE_MSN = 5;
const CHAT_TYPE_SKYPE = 6;
const CHAT_TYPE_YAHOO = 7;
const CHAT_TYPE_QQ = 8;
const CHAT_TYPE_WECHAT = 10;
const CHAT_TYPE_OTHER = 9;
// const CHAT_TYPE_AIM = 1;
// const CHAT_TYPE_GTALK = 2;
// const CHAT_TYPE_ICQ = 3;
// const CHAT_TYPE_XMPP = 4;
// const CHAT_TYPE_MSN = 5;
// const CHAT_TYPE_SKYPE = 6;
// const CHAT_TYPE_YAHOO = 7;
// const CHAT_TYPE_QQ = 8;
// const CHAT_TYPE_WECHAT = 10;
// const CHAT_TYPE_OTHER = 9;
/**
* For API.
*/
public static $chat_types = [
self::CHAT_TYPE_AIM => 'aim',
self::CHAT_TYPE_GTALK => 'gtalk',
self::CHAT_TYPE_ICQ => 'icq',
self::CHAT_TYPE_XMPP => 'xmpp',
self::CHAT_TYPE_MSN => 'msn',
self::CHAT_TYPE_SKYPE => 'skype',
self::CHAT_TYPE_YAHOO => 'yahoo',
self::CHAT_TYPE_QQ => 'qq',
self::CHAT_TYPE_WECHAT => 'wechat', // Extra
self::CHAT_TYPE_OTHER => 'other',
];
// public static $chat_types = [
// self::CHAT_TYPE_AIM => 'aim',
// self::CHAT_TYPE_GTALK => 'gtalk',
// self::CHAT_TYPE_ICQ => 'icq',
// self::CHAT_TYPE_XMPP => 'xmpp',
// self::CHAT_TYPE_MSN => 'msn',
// self::CHAT_TYPE_SKYPE => 'skype',
// self::CHAT_TYPE_YAHOO => 'yahoo',
// self::CHAT_TYPE_QQ => 'qq',
// self::CHAT_TYPE_WECHAT => 'wechat', // Extra
// self::CHAT_TYPE_OTHER => 'other',
// ];
public static $chat_type_names = [
self::CHAT_TYPE_AIM => 'AIM',
self::CHAT_TYPE_GTALK => 'Google+',
self::CHAT_TYPE_ICQ => 'ICQ',
self::CHAT_TYPE_XMPP => 'XMPP',
self::CHAT_TYPE_MSN => 'MSN',
self::CHAT_TYPE_SKYPE => 'Skype',
self::CHAT_TYPE_YAHOO => 'Yahoo',
self::CHAT_TYPE_QQ => 'QQ',
self::CHAT_TYPE_WECHAT => 'WeChat', // Extra
self::CHAT_TYPE_OTHER => 'Other',
];
// public static $chat_type_names = [
// self::CHAT_TYPE_AIM => 'AIM',
// self::CHAT_TYPE_GTALK => 'Google+',
// self::CHAT_TYPE_ICQ => 'ICQ',
// self::CHAT_TYPE_XMPP => 'XMPP',
// self::CHAT_TYPE_MSN => 'MSN',
// self::CHAT_TYPE_SKYPE => 'Skype',
// self::CHAT_TYPE_YAHOO => 'Yahoo',
// self::CHAT_TYPE_QQ => 'QQ',
// self::CHAT_TYPE_WECHAT => 'WeChat', // Extra
// self::CHAT_TYPE_OTHER => 'Other',
// ];
/**
* Social types.
*/
const SOCIAL_TYPE_TWITTER = 1;
const SOCIAL_TYPE_FACEBOOK = 2;
const SOCIAL_TYPE_TELEGRAM = 14;
const SOCIAL_TYPE_LINKEDIN = 3;
const SOCIAL_TYPE_ABOUTME = 4;
const SOCIAL_TYPE_GOOGLE = 5;
@@ -139,6 +146,7 @@ class Customer extends Model
public static $social_types = [
self::SOCIAL_TYPE_TWITTER => 'twitter',
self::SOCIAL_TYPE_FACEBOOK => 'facebook',
self::SOCIAL_TYPE_TELEGRAM => 'telegram',
self::SOCIAL_TYPE_LINKEDIN => 'linkedin',
self::SOCIAL_TYPE_ABOUTME => 'aboutme',
self::SOCIAL_TYPE_GOOGLE => 'google',
@@ -155,6 +163,7 @@ class Customer extends Model
public static $social_type_names = [
self::SOCIAL_TYPE_TWITTER => 'Twitter',
self::SOCIAL_TYPE_FACEBOOK => 'Facebook',
self::SOCIAL_TYPE_TELEGRAM => 'Telegram',
self::SOCIAL_TYPE_LINKEDIN => 'Linkedin',
self::SOCIAL_TYPE_ABOUTME => 'About.me',
self::SOCIAL_TYPE_GOOGLE => 'Google',
@@ -168,6 +177,13 @@ class Customer extends Model
self::SOCIAL_TYPE_OTHER => 'Other',
];
/**
* Search filters.
*/
public static $search_filters = [
'mailbox',
];
/**
* Countries list.
*/
@@ -424,6 +440,10 @@ class Customer extends Model
'ZW' => 'Zimbabwe',
];
protected $casts = [
'meta' => 'array',
];
/**
* Attributes which are not fillable using fill() method.
*/
@@ -434,7 +454,12 @@ class Customer extends Model
*
* @var [type]
*/
protected $fillable = ['first_name', 'last_name', 'company', 'job_title', 'address', 'city', 'state', 'zip', 'country'];
protected $fillable = ['first_name', 'last_name', 'company', 'job_title', 'address', 'city', 'state', 'zip', 'country', 'photo_url', 'age', 'gender', 'notes', 'channel', 'channel_id', 'social_profiles'];
/**
* Fields stored as JSON.
*/
protected $json_fields = ['phones', 'websites', 'social_profiles'];
/**
* Get customer emails.
@@ -444,6 +469,14 @@ class Customer extends Model
return $this->hasMany('App\Email');
}
/**
* Get customer emails.
*/
public function emails_cached()
{
return $this->hasMany('App\Email')->rememberForever();
}
/**
* Get customer conversations.
*/
@@ -457,7 +490,15 @@ class Customer extends Model
*/
public function getMainEmail()
{
return $this->emails()->first()->email;
return optional($this->emails_cached()->first())->email;
}
/**
* Get main email.
*/
public static function getMainEmailStatic($customer_id)
{
return Email::select('email')->where('customer_id', $customer_id)->pluck('email');
}
/**
@@ -465,7 +506,7 @@ class Customer extends Model
*
* @return string
*/
public function getFullName($email_if_empty = false)
public function getFullName($email_if_empty = false, $first_part_from_email = false)
{
if ($this->first_name && $this->last_name) {
return $this->first_name.' '.$this->last_name;
@@ -474,7 +515,12 @@ class Customer extends Model
} elseif (!$this->first_name && $this->last_name) {
return $this->last_name;
} elseif ($email_if_empty) {
return $this->getMainEmail();
$email = $this->getMainEmail();
if ($first_part_from_email) {
return $this->getNameFromEmail($email);
} else {
return $email;
}
}
return '';
@@ -501,11 +547,13 @@ class Customer extends Model
*
* @return string
*/
public function getNameFromEmail()
public function getNameFromEmail($email = '')
{
$email = $this->emails()->first();
if (!$email) {
$email = optional($this->emails_cached()->first())->email;
}
if ($email) {
return explode('@', $email->email)[0];
return explode('@', $email)[0];
} else {
return '';
}
@@ -558,15 +606,38 @@ class Customer extends Model
}
}
/**
* Add new email to customer.
*/
public function addEmail($email_address, $check_if_exists = false)
{
// Check if email already exists and belongs to another customer.
if ($check_if_exists) {
$email = Email::where('email', $email_address)->first();
if ($email && !empty($email->customer_id)) {
return false;
}
}
$new_email = new Email(['email' => $email_address]);
$this->emails()->save($new_email);
}
/**
* Get customers phones as array.
*
* @return array
*/
public function getPhones()
public function getPhones($dummy_if_empty = false)
{
if ($this->phones) {
return json_decode($this->phones);
$phones = json_decode($this->phones ?? '', true);
if (is_array($phones) && count($phones)) {
return $phones;
} elseif ($dummy_if_empty) {
return [[
'type' => self::PHONE_TYPE_WORK,
'value' => '',
]];
} else {
return [];
}
@@ -579,7 +650,19 @@ class Customer extends Model
*/
public function setPhones(array $phones_array)
{
$this->phones = json_encode(self::formatPhones($phones));
$phones_array = self::formatPhones($phones_array);
// Remove dubplicates.
$list = [];
foreach ($phones_array as $i => $data) {
if (in_array($data['value'], $list)) {
unset($phones_array[$i]);
} else {
$list[] = $data['value'];
}
}
$this->phones = \Helper::jsonEncodeUtf8($phones_array);
}
/**
@@ -592,16 +675,54 @@ class Customer extends Model
public static function formatPhones(array $phones_array)
{
$phones = [];
foreach ($phones_array as $phone) {
if (!empty($phone['value']) && !empty($phone['type']) && in_array($phone['type'], array_keys(self::$phone_types))) {
if (is_array($phone)) {
if (!empty($phone['value'])) {
if (empty($phone['type']) || !in_array($phone['type'], array_keys(self::$phone_types))) {
$phone['type'] = self::PHONE_TYPE_WORK;
}
$phones[] = [
'value' => (string) $phone['value'],
'type' => (int) $phone['type'],
];
}
} else {
$phones[] = [
'value' => (string) $phone['value'],
'type' => (int) $phone['type'],
'value' => (string) $phone,
'type' => self::PHONE_TYPE_WORK
];
}
}
return json_encode($phones);
return $phones;
}
/**
* Add website.
*/
public function addPhone($phone, $type = self::PHONE_TYPE_WORK)
{
if (is_string($phone)) {
$this->setPhones(array_merge(
$this->getPhones(),
[['value' => $phone, 'type' => $type]]
));
} else {
$this->setPhones(array_merge(
$this->getPhones(),
[$phone]
));
}
}
/**
* Find customer by phone number.
*/
public static function findByPhone($phone)
{
$phone = trim($phone);
return Customer::where('phones', 'LIKE', '%"'.$phone.'"%')->first();
}
/**
@@ -609,10 +730,17 @@ class Customer extends Model
*
* @return array
*/
public function getSocialProfiles()
public function getSocialProfiles($dummy_if_empty = false)
{
if ($this->social_profiles) {
return json_decode($this->social_profiles);
$social_profiles = json_decode($this->social_profiles ?? '', true);
if (is_array($social_profiles) && count($social_profiles)) {
return json_decode($this->social_profiles, true);
} elseif ($dummy_if_empty) {
return [[
'type' => '',
'value' => '',
]];
} else {
return [];
}
@@ -625,8 +753,9 @@ class Customer extends Model
*/
public function getWebsites($dummy_if_empty = false)
{
if ($this->websites) {
return json_decode($this->websites);
$websites = json_decode($this->websites ?? '', true);
if (is_array($websites) && count($websites)) {
return $websites;
} elseif ($dummy_if_empty) {
return [''];
} else {
@@ -643,17 +772,106 @@ class Customer extends Model
{
$websites = [];
foreach ($websites_array as $key => $value) {
$value = filter_var((string) $value, FILTER_SANITIZE_URL);
// FILTER_SANITIZE_URL cuts some symbols.
//$value = filter_var((string) $value, FILTER_SANITIZE_URL);
if (isset($value['value'])) {
$value = $value['value'];
}
if (!$value || preg_match("/^http(s)?:?\/?\/?$/i", $value)) {
continue;
}
if (!preg_match("/http(s)?:\/\//i", $value)) {
$value = 'http://'.$value;
}
$websites[] = (string) $value;
}
$this->websites = json_encode($websites);
$this->websites = \Helper::jsonEncodeUtf8(array_unique($websites));
}
/**
* Create customer or get existing.
* Add website.
*/
public function addWebsite($website)
{
$websites = $this->getWebsites();
if (isset($website['value'])) {
$website = $website['value'];
}
array_push($websites, $website);
$this->setWebsites($websites);
}
/**
* Sanitize social profiles.
*
* @param array $list [description]
*
* @return array [description]
*/
public static function formatSocialProfiles(array $list)
{
$social_profiles = [];
foreach ($list as $social_profile) {
if (is_array($social_profile)) {
if (!empty($social_profile['value']) && !empty($social_profile['type'])) {
$type = null;
if (is_numeric($social_profile['type']) && in_array($social_profile['type'], array_keys(self::$social_types))) {
$type = (int)$social_profile['type'];
} else {
// Find type.
foreach (self::$social_types as $type_id => $type_name) {
if ($type_name == strtolower($social_profile['type'])) {
$type = $type_id;
}
}
}
if (!$type) {
continue;
}
$social_profiles[] = [
'value' => (string) $social_profile['value'],
'type' => $type,
];
}
} else {
$social_profiles[] = [
'value' => (string) $social_profile,
'type' => self::SOCIAL_TYPE_OTHER,
];
}
}
return $social_profiles;
}
/**
* Set social profiles as JSON.
*
* @param array $websites_array
*/
public function setSocialProfiles(array $sp_array)
{
$sp_array = self::formatSocialProfiles($sp_array);
// Remove dubplicates.
$list = [];
foreach ($sp_array as $i => $data) {
if (in_array($data['value'], $list)) {
unset($sp_array[$i]);
} else {
$list[] = $data['value'];
}
}
$this->social_profiles = \Helper::jsonEncodeUtf8($sp_array);
}
/**
* Create customer or get existing and fill empty fields.
*
* @param string $email
* @param array $data [description]
@@ -662,6 +880,8 @@ class Customer extends Model
*/
public static function create($email, $data = [])
{
$new = false;
$email = Email::sanitizeEmail($email);
if (!$email) {
return null;
@@ -670,27 +890,194 @@ class Customer extends Model
if ($email_obj) {
$customer = $email_obj->customer;
if (empty($customer->first_name) && !empty($data['first_name'])) {
// In case somehow the email has no customer.
if (!$customer) {
// Customer will be saved and connected to the email later.
$customer = new self();
}
// Update name if empty.
/*if (empty($customer->first_name) && !empty($data['first_name'])) {
$customer->first_name = $data['first_name'];
if (empty($customer->last_name) && !empty($data['last_name'])) {
$customer->last_name = $data['last_name'];
}
$customer->save();
}
}*/
} else {
$customer = new self();
$email_obj = new Email();
$email_obj->email = $email;
$customer->fill($data);
$new = true;
}
// Set empty fields
if ($customer->setData($data, false) || !$customer->id) {
$customer->save();
}
if (empty($email_obj->id) || !$email_obj->customer_id) {
$email_obj->customer()->associate($customer);
$email_obj->save();
if (empty($email_obj->id) || !$email_obj->customer_id || $email_obj->customer_id != $customer->id) {
// Email may have been set in setData().
$save_email = true;
if (!empty($data['emails']) && is_array($data['emails'])) {
foreach ($data['emails'] as $data_email) {
if (is_string($data_email) && $data_email == $email) {
$save_email = false;
break;
}
if (is_array($data_email) && !empty($data_email['value']) && $data_email['value'] == $email) {
$save_email = false;
break;
}
}
}
if ($save_email) {
$email_obj->customer()->associate($customer);
$email_obj->save();
}
}
// Todo: check phone uniqueness.
if ($new) {
\Eventy::action('customer.created', $customer);
}
return $customer;
}
/**
* Set empty fields.
*/
public function setData($data, $replace_data = true, $save = false)
{
$result = false;
// todo: photoUrl.
if (isset($data['photo_url'])) {
unset($data['photo_url']);
}
if (!empty($data['background']) && empty($data['notes'])) {
$data['notes'] = $data['background'];
}
if ($replace_data) {
// Replace data.
$data_prepared = $data;
foreach ($data_prepared as $i => $value) {
if (is_array($value)) {
unset($data_prepared[$i]);
}
}
$this->fill($data_prepared);
$result = true;
} else {
// Update empty fields.
foreach ($data as $key => $value) {
if (in_array($key, $this->fillable) && empty($this->$key)) {
$this->$key = $value;
$result = true;
}
}
}
// Set JSON values.
if (!empty($data['phone'])) {
$this->addPhone($data['phone']);
}
foreach ($data as $key => $value) {
if (!in_array($key, $this->json_fields) && $key != 'emails') {
continue;
}
if ($key == 'phones') {
if (isset($value['value'])) {
$this->addPhone($value);
} else {
$this->setPhones($value);
// foreach ($value as $phone_value) {
// $this->addPhone($phone_value);
// }
}
$result = true;
}
if ($key == 'websites') {
if (is_array($value)) {
$this->setWebsites($value);
// foreach ($value as $website) {
// $this->addWebsite($website);
// }
} else {
$this->addWebsite($value);
}
$result = true;
}
if ($key == 'social_profiles') {
$this->setSocialProfiles($value);
$result = true;
}
if ($key == 'country') {
if (array_search($this->country, Customer::$countries)) {
$this->country = array_search($this->country, Customer::$countries);
}
$this->country = strtoupper(mb_substr($this->country, 0, 2));
$result = true;
}
}
// Emails must be processed the last as they need to save object.
foreach ($data as $key => $value) {
if ($key == 'emails') {
foreach ($value as $email_data) {
if (is_string($email_data)) {
if (!$this->id) {
$this->save();
}
$email_created = Email::create($email_data, $this->id, Email::TYPE_WORK);
if ($email_created) {
$result = true;
}
} elseif (!empty($email_data['value'])) {
if (!$this->id) {
$this->save();
}
$email_created = Email::create($email_data['value'], $this->id, $email_data['type']);
if ($email_created) {
$result = true;
}
}
}
break;
}
}
// Maybe Todo: check phone uniqueness.
// Same phone can be written in many ways, so it's almost useless to chek uniqueness.
\Eventy::action('customer.set_data', $this, $data, $replace_data);
if ($save) {
$this->save();
}
return $result;
}
/**
* Create a customer, email is not required.
* For phone conversations.
*/
public static function createWithoutEmail($data = [])
{
$customer = new self();
$customer->setData($data);
$customer->save();
\Eventy::action('customer.created', $customer);
return $customer;
}
@@ -704,6 +1091,16 @@ class Customer extends Model
return route('customers.update', ['id'=>$this->id]);
}
/**
* Get view customer URL.
*
* @return string
*/
public function urlView()
{
return route('customers.conversations', ['id'=>$this->id]);
}
/**
* Format date according to customer's timezone.
*
@@ -717,9 +1114,274 @@ class Customer extends Model
return $date->format($format);
}
public function getPhotoUrl()
/**
* Get full representation of customer.
*/
public function getEmailAndName()
{
// todo
return '/img/default-avatar.png';
// Email can be fetched using query.
$text = $this->email;
if (!$text) {
$text = $this->getMainEmail();
}
if ($this->getFullName()) {
if ($text) {
$text .= ' ('.$this->getFullName().')';
} else {
$text .= $this->getFullName();
}
}
return $text;
}
public function getNameAndEmail()
{
// Email can be fetched using query.
$text = $this->getFullName();
$email = $this->email;
if (!$email) {
$email = $this->getMainEmail();
}
if ($email) {
if ($text) {
$text .= ' <'.$email.'>';
} else {
$text .= $email;
}
}
return $text;
}
/**
* Get customers info for the list of emails.
*/
public static function emailsToCustomers($list)
{
$result = [];
$data = Customer::select(['emails.email', 'customers.first_name', 'customers.last_name'])
->join('emails', function ($join) {
$join->on('emails.customer_id', '=', 'customers.id');
})
->whereIn('emails.email', $list)
//->groupby('customers.id')
->get()
->toArray();
foreach ($list as $email) {
// Dummy customer.
$customer = new Customer();
$customer->email = $email;
foreach ($data as $values) {
if (strtolower($values['email']) == strtolower($email)) {
$customer->first_name = $values['first_name'];
$customer->last_name = $values['last_name'];
break;
}
}
$result[$email] = $customer->getNameAndEmail();
}
return $result;
}
/**
* Get customer by email.
*/
public static function getByEmail($email)
{
return Customer::select('customers.*')
->where('emails.email', $email)
->join('emails', function ($join) {
$join->on('emails.customer_id', '=', 'customers.id');
})->first();
}
/**
* Get email or phone if email is empty.
*/
public function getEmailOrPhone()
{
if (!empty($this->email)) {
// Email can be selected with query.
return $this->email;
} elseif ($main_email = $this->getMainEmail()) {
return $main_email;
} elseif ($phones = $this->getPhones() && !empty($phones[0]['value'])) {
return $phones[0]['value'];
}
return '';
}
public function getPhotoUrl($default_if_empty = true)
{
if (!empty($this->photo_url) || !$default_if_empty) {
if (!empty($this->photo_url)) {
return self::getPhotoUrlByFileName($this->photo_url);
} else {
return '';
}
} else {
return asset('/img/default-avatar.png');
}
}
public static function getPhotoUrlByFileName($file_name)
{
return Storage::url(self::PHOTO_DIRECTORY.DIRECTORY_SEPARATOR.$file_name);
}
/**
* Resize and save photo.
*/
public function savePhoto($real_path, $mime_type)
{
$resized_image = \App\Misc\Helper::resizeImage($real_path, $mime_type, self::PHOTO_SIZE, self::PHOTO_SIZE);
if (!$resized_image) {
return false;
}
$file_name = md5(Hash::make($this->id)).'.jpg';
$dest_path = Storage::path(self::PHOTO_DIRECTORY.DIRECTORY_SEPARATOR.$file_name);
$dest_dir = pathinfo($dest_path, PATHINFO_DIRNAME);
if (!file_exists($dest_dir)) {
\File::makeDirectory($dest_dir, 0755);
}
// Remove current photo
if ($this->photo_url) {
Storage::delete(self::PHOTO_DIRECTORY.DIRECTORY_SEPARATOR.$this->photo_url);
}
imagejpeg($resized_image, $dest_path, self::PHOTO_QUALITY);
return $file_name;
}
/**
* Remove user photo.
*/
public function removePhoto()
{
if ($this->photo_url) {
Storage::delete(self::PHOTO_DIRECTORY.DIRECTORY_SEPARATOR.$this->photo_url);
}
$this->photo_url = '';
}
public function getCountryName()
{
if ($this->country && !empty(self::$countries[$this->country])) {
return self::$countries[$this->country];
} else {
return '';
}
}
/**
* Get first and last name.
*/
public static function parseName($name)
{
$data = [];
if (!$name) {
return $data;
}
$name_parts = explode(' ', $name, 2);
$data['first_name'] = $name_parts[0];
if (!empty($name_parts[1])) {
$data['last_name'] = $name_parts[1];
}
return $data;
}
public static function formatSocialProfile($sp)
{
if (empty($sp['type']) || !isset(self::$social_type_names[$sp['type']])) {
$sp['type'] = self::SOCIAL_TYPE_OTHER;
}
$sp['type_name'] = self::$social_type_names[$sp['type']];
$sp['value_url'] = $sp['value'];
if (!preg_match("/^https?:\/\//i", $sp['value_url'])) {
switch ($sp['type']) {
case self::SOCIAL_TYPE_TELEGRAM:
$sp['value_url'] = 'https://t.me/'.$sp['value'];
break;
default:
$sp['value_url'] = 'http://'.$sp['value_url'];
break;
}
}
if (empty($sp['value_url'])) {
$sp['value_url'] = '';
}
return $sp;
}
public function setPhotoFromRemoteFile($url)
{
$headers = get_headers($url);
if (!preg_match("/200/", $headers[0])) {
return false;
}
$image_data = file_get_contents($url);
if (!$image_data) {
return false;
}
$temp_file = tempnam(sys_get_temp_dir(), 'photo');
\File::put($temp_file, $image_data);
$photo_url = $this->savePhoto($temp_file, \File::mimeType($temp_file));
if ($photo_url) {
$this->photo_url = $photo_url;
return true;
} else {
return false;
}
}
public function getChannelName()
{
return \Eventy::filter('channel.name', '', $this->channel);
}
/**
* Get thread meta value.
*/
public function getMeta($key, $default = null)
{
if (isset($this->meta[$key])) {
return $this->meta[$key];
} else {
return $default;
}
}
/**
* Set thread meta value.
*/
public function setMeta($key, $value)
{
$meta = $this->meta;
$meta[$key] = $value;
$this->meta = $meta;
}
}

View File

@@ -3,9 +3,14 @@
namespace App;
use Illuminate\Database\Eloquent\Model;
use Watson\Rememberable\Rememberable;
class Email extends Model
{
use Rememberable;
// This is obligatory.
public $rememberCacheDriver = 'array';
/**
* Email types.
*/
@@ -47,11 +52,15 @@ class Email extends Model
// Email validation is not recommended:
// http://stackoverflow.com/questions/201323/using-a-regular-expression-to-validate-an-email-address/201378#201378
// So we just check for @
if (!preg_match('/@/', $email)) {
if (!preg_match('/@/', $email ?? '')) {
return false;
}
$email = filter_var($email, FILTER_SANITIZE_EMAIL);
$email = mb_strtolower($email, 'UTF-8');
// Remove trailing dots.
$email = preg_replace("/\.+$/", '', $email);
// Remove dot before @
$email = preg_replace("/\.+@/", '@', $email);
return $email;
}
@@ -60,4 +69,19 @@ class Email extends Model
{
return explode('@', $this->email)[0];
}
public static function create($email, $customer_id, $type = self::TYPE_WORK)
{
try {
$email_obj = new Email();
$email_obj->email = $email;
$email_obj->type = array_key_exists($type, self::$types) ? $type : self::TYPE_WORK;
$email_obj->customer_id = $customer_id;
$email_obj->save();
return $email_obj;
} catch (\Exception $e) {
return null;
}
}
}

View File

@@ -3,17 +3,15 @@
namespace App\Events;
use Illuminate\Broadcasting\Channel;
use Illuminate\Queue\SerializesModels;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
use Illuminate\Queue\SerializesModels;
class RealtimeBroadcastNotificationCreated implements ShouldBroadcastNow
{
use SerializesModels;
/**
/**
* The notifiable entity who received the notification.
*
* @var mixed
@@ -55,7 +53,7 @@ class RealtimeBroadcastNotificationCreated implements ShouldBroadcastNow
{
$channels = $this->notification->broadcastOn();
if (! empty($channels)) {
if (!empty($channels)) {
return $channels;
}
@@ -72,7 +70,7 @@ class RealtimeBroadcastNotificationCreated implements ShouldBroadcastNow
public function broadcastWith()
{
return array_merge($this->data, [
'id' => $this->notification->id,
'id' => $this->notification->id,
'type' => get_class($this->notification),
]);
// return [
@@ -95,4 +93,4 @@ class RealtimeBroadcastNotificationCreated implements ShouldBroadcastNow
return $class.'.'.$this->notifiable->getKey();
}
}
}

View File

@@ -0,0 +1,118 @@
<?php
/**
* New thread created in conversation.
*/
namespace App\Events;
use App\Conversation;
use App\Mailbox;
use App\Thread;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
use Illuminate\Queue\SerializesModels;
class RealtimeConvNewThread 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['conversation_id'])) {
return 'conv.'.$this->data['conversation_id'];
} else {
return 'conv.0';
}
}
/**
* Helper funciton.
*/
public static function dispatchSelf($thread)
{
if ($thread->state != Thread::STATE_PUBLISHED) {
return;
}
$notification_data = [
'thread_id' => $thread->id,
'conversation_id' => $thread->conversation_id,
// conversation is prefetched in ThreadObserver.
'mailbox_id' => $thread->conversation->mailbox_id,
//'user_id' => $thread->created_by_user_id,
];
event(new \App\Events\RealtimeConvNewThread($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 [];
}
$thread = Thread::find($payload->thread_id);
if (!$thread) {
return $payload;
}
// Add thread html to the payload.
$template_data = [
'conversation' => $thread->conversation,
'mailbox' => $thread->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];
return $payload;
}
}

View File

@@ -0,0 +1,113 @@
<?php
namespace App\Events;
use Illuminate\Broadcasting\Channel;
//use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
use Illuminate\Queue\SerializesModels;
class RealtimeConvView implements ShouldBroadcastNow
{
use SerializesModels;
/**
* The notifiable entity who received the notification.
*
* @var mixed
*/
//public $notifiable;
/**
* The notification instance.
*
* @var \Illuminate\Notifications\Notification
*/
//public $notification;
/**
* The notification data.
*
* @var array
*/
public $data = [];
/**
* Create a new event instance.
*
* @return void
*/
public function __construct(/*$notifiable, $notification,*/ $data)
{
$this->data = $data;
// $this->notifiable = $notifiable;
// $this->notification = $notification;
}
/**
* Get the channels the event should broadcast on.
*
* @return Channel|array
*/
public function broadcastOn()
{
// $channels = $this->notification->broadcastOn();
// if (!empty($channels)) {
// return $channels;
// }
return new \Illuminate\Broadcasting\Channel($this->channelName());
//return [new PrivateChannel()];
}
/**
* Get the data to broadcast.
*
* @return array
*/
public function broadcastWith()
{
return $this->data;
/*return array_merge($this->data, [
'id' => $this->notification->id,
'type' => get_class($this->notification),
]);*/
}
/**
* Get the broadcast channel name for the event.
*
* @return string
*/
protected function channelName()
{
// if (method_exists($this->notifiable, 'receivesBroadcastNotificationsOn')) {
// return $this->notifiable->receivesBroadcastNotificationsOn($this->notification);
// }
return 'conv';
// $class = str_replace('\\', '.', get_class($this->notifiable));
// return $class.'.'.$this->notifiable->getKey();
}
/**
* Helper funciton.
*/
public static function dispatchSelf($conversation_id, $user, $replying = false)
{
$notification_data = [
'conversation_id' => $conversation_id,
'user_id' => $user->id,
'user_photo_url' => $user->getPhotoUrl(false),
// These has to be encoded to avoid "Unable to JSON encode payload. Error code: 5"
'user_initials' => htmlentities($user->getInitials()),
'user_name' => htmlentities($user->getFullName()),
'replying' => (int)$replying,
];
event(new \App\Events\RealtimeConvView($notification_data));
}
}

View File

@@ -0,0 +1,62 @@
<?php
/**
* User finished viewing conversatin.
*/
namespace App\Events;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
use Illuminate\Queue\SerializesModels;
class RealtimeConvViewFinish 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()
{
return 'conv';
}
}

View File

@@ -0,0 +1,110 @@
<?php
/**
* New thread created in mailbox.
*/
namespace App\Events;
use App\Conversation;
use App\Mailbox;
use App\Folder;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
use Illuminate\Queue\SerializesModels;
class RealtimeMailboxNewThread 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 'mailbox.'.$this->data['mailbox_id'];
} else {
return 'mailbox.0';
}
}
/**
* Helper funciton.
*/
public static function dispatchSelf($mailbox_id)
{
$notification_data = [
'mailbox_id' => $mailbox_id
];
event(new \App\Events\RealtimeMailboxNewThread($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 [];
}
$folder = null;
$foler_id = Conversation::getFolderParam();
if ($foler_id) {
$folder = Folder::find($foler_id);
}
// Just in case.
if (!$folder) {
$folder = new Folder();
}
$template_data = [
'folders' => $mailbox->getAssesibleFolders(),
'folder' => $folder,
'mailbox' => $mailbox,
];
$payload->folders_html = \View::make('mailboxes/partials/folders')->with($template_data)->render();
return $payload;
}
}

View File

@@ -6,7 +6,7 @@ use App\User;
class UserDeleted
{
public $user;
public $by_user;
public $deleted_user;
/**
@@ -14,9 +14,11 @@ class UserDeleted
*
* @return void
*/
public function __construct(User $user, User $deleted_user)
public function __construct(User $deleted_user, User $by_user)
{
$this->user = $user;
$this->by_user = $by_user;
$this->deleted_user = $deleted_user;
\Eventy::action('user.deleted', $deleted_user, $by_user);
}
}

View File

@@ -23,12 +23,12 @@ class Folder extends Model
public static $types = [
self::TYPE_UNASSIGNED => 'Unassigned',
self::TYPE_MINE => 'Mine',
self::TYPE_STARRED => 'Starred',
self::TYPE_DRAFTS => 'Drafts',
self::TYPE_ASSIGNED => 'Assigned',
self::TYPE_CLOSED => 'Closed',
self::TYPE_SPAM => 'Spam',
self::TYPE_DELETED => 'Deleted',
self::TYPE_STARRED => 'Starred',
];
/**
@@ -68,8 +68,16 @@ class Folder extends Model
self::TYPE_STARRED,
];
// Counter mode.
const COUNTER_ACTIVE = 1;
const COUNTER_TOTAL = 2;
public $timestamps = false;
protected $casts = [
'meta' => 'array',
];
/**
* Get the mailbox to which folder belongs.
*/
@@ -96,12 +104,32 @@ class Folder extends Model
public function getTypeName()
{
return __(self::$types[$this->type]);
// To make name translatable.
switch ($this->type) {
case self::TYPE_UNASSIGNED:
return __('Unassigned');
case self::TYPE_MINE:
return __('Mine');
case self::TYPE_DRAFTS:
return __('Drafts');
case self::TYPE_ASSIGNED:
return __('Assigned');
case self::TYPE_CLOSED:
return __('Closed');
case self::TYPE_SPAM:
return __('Spam');
case self::TYPE_DELETED:
return __('Deleted');
case self::TYPE_STARRED:
return __('Starred');
default:
return __(\Eventy::filter('folder.type_name', self::$types[$this->type] ?? '', $this));
}
}
public function getTypeIcon()
{
return self::$type_icons[$this->type];
return \Eventy::filter('folder.type_icon', self::$type_icons[$this->type] ?? '', $this);
}
/**
@@ -124,7 +152,6 @@ class Folder extends Model
case self::TYPE_STARRED:
$order_by[] = ['status' => 'asc'];
$order_by[] = ['last_reply_at' => 'desc'];
//$order_by = [['conversation_folder.id' => 'desc']];
break;
case self::TYPE_DRAFTS:
@@ -141,6 +168,30 @@ class Folder extends Model
case self::TYPE_DELETED:
$order_by = [['user_updated_at' => 'desc']];
break;
default:
$order_by = \Eventy::filter('folder.conversations_order_by', $order_by, $this->type);
break;
}
// Process columns sorting.
$sorting = Conversation::getConvTableSorting();
if ($sorting['sort_by'] == 'date') {
if ($sorting['order'] != 'desc') {
foreach ($order_by as $block_i => $block) {
foreach ($block as $field => $order) {
if ($field == 'status') {
unset($order_by[$block_i][$field]);
} else {
if ($order == 'desc') {
$order_by[$block_i][$field] = 'asc';
}
}
}
}
}
} else {
$order_by = [[$sorting['sort_by'] => $sorting['order']]];
}
return $order_by;
@@ -166,31 +217,38 @@ class Folder extends Model
*/
public function isIndirect()
{
return in_array($this->type, Folder::$indirect_types);
return in_array($this->type, self::$indirect_types);
}
public function updateCounters()
{
if ($this->type == Folder::TYPE_MINE && $this->user_id) {
if (\Eventy::filter('folder.update_counters', false, $this)) {
return;
}
if ($this->type == self::TYPE_MINE && $this->user_id) {
$this->active_count = Conversation::where('user_id', $this->user_id)
->where('mailbox_id', $this->mailbox_id)
->where('status', Conversation::STATUS_ACTIVE)
->where('state', Conversation::STATE_PUBLISHED)
->where('status', Conversation::STATUS_ACTIVE)
->count();
$this->total_count = Conversation::where('user_id', $this->user_id)
->where('mailbox_id', $this->mailbox_id)
->where('state', Conversation::STATE_PUBLISHED)
->count();
} elseif ($this->type == Folder::TYPE_STARRED) {
} elseif ($this->type == self::TYPE_STARRED) {
$this->active_count = count(Conversation::getUserStarredConversationIds($this->mailbox_id, $this->user_id));
$this->total_count = $this->active_count;
} elseif ($this->type == Folder::TYPE_DELETED) {
} elseif ($this->type == self::TYPE_DELETED) {
$this->active_count = $this->conversations()->where('state', Conversation::STATE_DELETED)
->count();
$this->total_count = $this->active_count;
} elseif ($this->isIndirect()) {
// Conversation are connected to folder via conversation_folder table.
$this->active_count = ConversationFolder::where('folder_id', $this->id)->count();
// Drafts.
$this->active_count = ConversationFolder::where('conversation_folder.folder_id', $this->id)
->join('conversations', 'conversations.id', '=', 'conversation_folder.conversation_id')
//->where('state', Conversation::STATE_PUBLISHED)
->count();
$this->total_count = $this->active_count;
} else {
$this->active_count = $this->conversations()
@@ -204,19 +262,46 @@ class Folder extends Model
$this->save();
}
/**
* Get count to display in folders list.
*
* @param array $folders [description]
*
* @return [type] [description]
*/
public function getCount($folders = [])
{
$counter = \Eventy::filter('folder.counter', self::COUNTER_ACTIVE, $this, $folders);
$count = \Eventy::filter('folder.count', false, $this, $counter, $folders);
if ($count !== false) {
return $count;
}
if ($counter == self::COUNTER_TOTAL || $this->type == self::TYPE_STARRED || $this->type == self::TYPE_DRAFTS) {
return $this->total_count;
} else {
return $this->getActiveCount($folders);
}
}
/**
* Get calculated number of active conversations.
*/
public function getActiveCount($folders = [])
{
$active_count = $this->active_count;
if ($this->type == Folder::TYPE_ASSIGNED) {
if ($folders) {
$mine_folder = $folders->firstWhere('type', Folder::TYPE_MINE);
} else {
$mine_folder = $this->mailbox->folders()->where('type', Folder::TYPE_MINE)->first();
if ($this->type == self::TYPE_ASSIGNED) {
$mine_folder = \Eventy::filter('folder.active_count_mine_folder', null, $this, $folders);
if (!$mine_folder) {
if ($folders) {
$mine_folder = $folders->firstWhere('type', self::TYPE_MINE);
} elseif ($this->mailbox_id) {
$mine_folder = $this->mailbox->folders()->where('type', self::TYPE_MINE)->first();
}
}
if ($mine_folder) {
$active_count = $active_count - $mine_folder->active_count;
if ($active_count < 0) {
@@ -227,4 +312,100 @@ class Folder extends Model
return $active_count;
}
/**
* Query for waiting since.
*/
public function getWaitingSinceQuery()
{
$query = null;
if ($this->type == self::TYPE_MINE) {
// Assigned to user.
$query = Conversation::where('user_id', $this->user_id)
->where('mailbox_id', $this->mailbox_id);
} elseif ($this->isIndirect()) {
// Via intermediate table.
$query = Conversation::join('conversation_folder', 'conversations.id', '=', 'conversation_folder.conversation_id')
->where('conversation_folder.folder_id', $this->id);
} else {
// All other conversations.
$query = $this->conversations();
}
return \Eventy::filter('folder.waiting_since_query', $query, $this);
}
/**
* Works for main folder only for now.
*
* @return [type] [description]
*/
public function getWaitingSince()
{
// Get oldest active conversation.
$conversation = $this->getWaitingSinceQuery()
->where('state', Conversation::STATE_PUBLISHED)
->where('status', Conversation::STATUS_ACTIVE)
->orderBy($this->getWaitingSinceField(), 'asc')
->first();
if ($conversation) {
return $conversation->getWaitingSince($this);
} else {
return '';
}
}
/**
* Get conversation field used to detect waiting since time.
*
* @return [type] [description]
*/
public function getWaitingSinceField()
{
if ($this->type == \App\Folder::TYPE_CLOSED) {
return 'closed_at';
} elseif ($this->type == \App\Folder::TYPE_DRAFTS) {
return 'updated_at';
} elseif ($this->type == \App\Folder::TYPE_DELETED) {
return 'user_updated_at';
} else {
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);
}
public static function create($data, $unique_per_user = true, $save = true)
{
if (!isset($data['mailbox_id']) || !isset($data['type'])) {
return null;
}
$folder = new Folder ();
$folder->mailbox_id = $data['mailbox_id'];
$folder->type = $data['type'];
if (!empty($data['user_id'])) {
if ($unique_per_user) {
$user_folder = Folder::where('mailbox_id', $data['mailbox_id'])
->where('user_id', $data['user_id'])
->where('type', $data['type'])
->first();
// User folder already exists.
if ($user_folder) {
return $user_folder;
}
}
$folder->user_id = $data['user_id'];
}
if ($save) {
$folder->save();
}
return $folder;
}
}

10
app/Follower.php Normal file
View File

@@ -0,0 +1,10 @@
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Follower extends Model
{
public $timestamps = false;
}

View File

@@ -25,7 +25,7 @@ class LoginController extends Controller
*
* @var string
*/
protected $redirectTo = '/';
protected $redirectTo = '/home';
/**
* Create a new controller instance.

1725
app/Http/Controllers/ConversationsController.php Normal file → Executable file

File diff suppressed because it is too large Load Diff

View File

@@ -6,7 +6,6 @@ use App\Conversation;
use App\Customer;
use App\Email;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use Validator;
class CustomersController extends Controller
@@ -68,12 +67,26 @@ class CustomersController extends Controller
//'emails' => 'array|required_without:first_name',
//'emails.1' => 'nullable|email|required_without:first_name',
'emails.*' => 'nullable|email|distinct|required_without:first_name',
'photo_url' => 'nullable|image|mimes:jpeg,png,jpg,gif',
]);
$validator->setAttributeNames([
//'emails.1' => __('Email'),
'photo_url' => __('Photo'),
'emails.*' => __('Email'),
]);
// 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());
if ($path_url) {
$customer->photo_url = $path_url;
} else {
$validator->errors()->add('photo_url', __('Error occured processing the image. Make sure that PHP GD extension is enabled.'));
}
}
});
if ($validator->fails()) {
return redirect()->route('customers.update', ['id' => $id])
->withErrors($validator)
@@ -96,12 +109,12 @@ class CustomersController extends Controller
foreach ($new_emails as $new_email) {
$email = Email::where('email', $new_email)->first();
if ($email && $email->customer) {
// If customer whose email is removed does not have first name and other emails
// 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) {
if ($request->first_name) {
$email->customer->first_name = $request->first_name;
} else if ($customer->first_name) {
} elseif ($customer->first_name) {
$email->customer->first_name = $customer->first_name;
} else {
$email->customer->first_name = mb_ucfirst($email->getNameFromEmail());
@@ -110,12 +123,12 @@ class CustomersController extends Controller
}
$flash_message .= __('Email :tag_email_begin:email:tag_email_end has been moved from another customer: :a_begin:customer:a_end.', [
'email' => $email->email,
'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">',
'a_end' => '</a></strong>',
'tag_email_end' => '</strong>',
'customer' => $email->customer->getFullName(),
'a_begin' => '<strong><a href="'.$email->customer->url().'" target="_blank">',
'a_end' => '</a></strong>',
]).' ';
$new_emails_change_customer[] = $email->email;
@@ -129,11 +142,17 @@ class CustomersController extends Controller
}
}
$customer->fill($request->all());
// Websites
if (!empty($request->websites)) {
$customer->setWebsites($request->websites);
$request_data = $request->all();
if (isset($request_data['photo_url'])) {
unset($request_data['photo_url']);
}
$customer->setData($request_data);
// Websites
// if (!empty($request->websites)) {
// $customer->setWebsites($request->websites);
// }
$customer->save();
$customer->syncEmails($request->emails);
@@ -159,8 +178,12 @@ class CustomersController extends Controller
}
}
\Eventy::action('customer.updated', $customer);
$flash_message = __('Customer saved successfully.').' '.$flash_message;
\Session::flash('flash_success_unescaped', $flash_message);
\Session::flash('customer.updated', 1);
return redirect()->route('customers.update', ['id' => $id]);
}
@@ -207,12 +230,13 @@ class CustomersController extends Controller
$conversations = $customer->conversations()
->where('customer_id', $customer->id)
->whereIn('mailbox_id', auth()->user()->mailboxesIdsCanView())
->orderBy('created_at', 'desc')
->paginate(Conversation::DEFAULT_LIST_SIZE);
return view('customers/conversations', [
'customer' => $customer,
'conversations' => $conversations
'conversations' => $conversations,
]);
}
@@ -222,33 +246,94 @@ class CustomersController extends Controller
public function ajaxSearch(Request $request)
{
$response = [
'results' => [],
'pagination' => ['more' => false]
'results' => [],
'pagination' => ['more' => false],
];
$q = $request->q;
$customers = Customer::select('customers.id', 'emails.email', 'first_name', 'last_name')
->join('emails', 'customers.id', '=', 'emails.customer_id')
->where('emails.email', 'like', '%'.$q.'%')
->where('emails.email', '<>', $request->exclude_email)
->orWhere('first_name', 'like', '%'.$q.'%')
->orWhere('last_name', 'like', '%'.$q.'%')
->paginate(20);
$join_emails = false;
if ($request->search_by == 'all' || $request->search_by == 'email' || $request->exclude_email) {
$join_emails = true;
}
$select_list = ['customers.id', 'first_name', 'last_name'];
if ($join_emails) {
$select_list[] = 'emails.email';
}
if ($request->show_fields == 'phone') {
$select_list[] = 'phones';
}
$customers_query = Customer::select($select_list);
if ($join_emails) {
if ($request->allow_non_emails) {
$customers_query->leftJoin('emails', 'customers.id', '=', 'emails.customer_id');
} else {
$customers_query->join('emails', 'customers.id', '=', 'emails.customer_id');
}
}
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 = $customers_query->paginate(20);
foreach ($customers as $customer) {
$text = $customer->email;
if ($customer->getFullName()) {
$text .= ' ('.$customer->getFullName().')';
}
if (!empty($request->use_id)) {
$id = $customer->id;
$id = '';
$text = '';
if ($request->show_fields != 'all') {
switch ($request->show_fields) {
case 'email':
$text = $customer->email;
break;
case 'name':
$text = $customer->getFullName();
break;
case 'phone':
// Get phone which matches
$phones = $customer->getPhones();
foreach ($phones as $phone) {
if (strstr($phone['value'], $q)) {
$text = $phone['value'];
if ($customer->getFullName()) {
$text .= ' — '.$customer->getFullName();
}
$id = $phone['value'];
break;
}
}
break;
default:
$text = $customer->getNameAndEmail();
break;
}
} else {
$id = $customer->email;
$text = $customer->getNameAndEmail();
}
if (!$id) {
if (!empty($request->use_id)) {
$id = $customer->id;
} else {
$id = $customer->email;
}
}
$response['results'][] = [
'id' => $id,
'text' => $text
'id' => $id,
'text' => $text,
];
}
@@ -256,4 +341,69 @@ class CustomersController extends Controller
return \Response::json($response);
}
/**
* Ajax controller.
*/
public function ajax(Request $request)
{
$response = [
'status' => 'error',
'msg' => '', // this is error message
];
$user = auth()->user();
switch ($request->action) {
// Change conversation user
case 'create':
// First name or email must be specified
$validator = Validator::make($request->all(), [
'first_name' => 'required|string|max:255',
'last_name' => 'nullable|string|max:255',
'email' => 'required|email|unique:emails,email',
]);
if ($validator->fails()) {
foreach ($validator->errors()->getMessages()as $errors) {
foreach ($errors as $field => $message) {
$response['msg'] .= $message.' ';
}
}
}
if (!$response['msg']) {
$customer = Customer::create($request->email, $request->all());
if ($customer) {
$response['email'] = $request->email;
$response['status'] = 'success';
}
}
break;
// Conversations navigation
case 'customers_pagination':
$customers = app('App\Http\Controllers\ConversationsController')->searchCustomers($request, $user);
$response['status'] = 'success';
$response['html'] = view('customers/partials/customers_table', [
'customers' => $customers,
])->render();
break;
default:
$response['msg'] = 'Unknown action';
break;
}
if ($response['status'] == 'error' && empty($response['msg'])) {
$response['msg'] = 'Unknown error occured';
}
return \Response::json($response);
}
}

540
app/Http/Controllers/MailboxesController.php Normal file → Executable file
View File

@@ -29,9 +29,17 @@ class MailboxesController extends Controller
*/
public function mailboxes()
{
//$this->authorize('create', 'App\Mailbox');
//$mailboxes = Mailbox::all();
$mailboxes = auth()->user()->mailboxesCanView();
$user = auth()->user();
$mailboxes = $user->mailboxesCanView();
if (!\Eventy::filter('user.can_view_mailbox_menu', false, $user)) {
foreach ($mailboxes as $i => $mailbox) {
if (!$user->canManageMailbox($mailbox->id)) {
$mailboxes->forget($i);
}
}
}
return view('mailboxes/mailboxes', ['mailboxes' => $mailboxes]);
}
@@ -43,7 +51,7 @@ class MailboxesController extends Controller
{
$this->authorize('create', 'App\Mailbox');
$users = User::where('role', '!=', User::ROLE_ADMIN)->get();
$users = User::nonDeleted()->where('role', '!=', User::ROLE_ADMIN)->get();
return view('mailboxes/create', ['users' => $users]);
}
@@ -55,6 +63,8 @@ class MailboxesController extends Controller
*/
public function createSave(Request $request)
{
$invalid = false;
$this->authorize('create', 'App\Mailbox');
$validator = Validator::make($request->all(), [
@@ -64,7 +74,12 @@ class MailboxesController extends Controller
// //event(new Registered($user = $this->create($request->all())));
if ($validator->fails()) {
if (Mailbox::userEmailExists($request->email)) {
$invalid = true;
$validator->errors()->add('email', __('There is a user with such email. Users and mailboxes can not have the same email addresses.'));
}
if ($invalid || $validator->fails()) {
return redirect()->route('mailboxes.create')
->withErrors($validator)
->withInput();
@@ -88,9 +103,24 @@ class MailboxesController extends Controller
public function update($id)
{
$mailbox = Mailbox::findOrFail($id);
//$this->authorize('update', $mailbox);
if (!auth()->user()->can('update', $mailbox)) {
$accessible_route = \Eventy::filter('mailbox.accessible_settings_route', '', auth()->user(), $mailbox);
$user = auth()->user();
if (!$user->can('updateSettings', $mailbox) && !$user->can('updateEmailSignature', $mailbox)) {
$accessible_route = '';
$mailbox_settings = $user->mailboxSettings($mailbox->id);
$access_permissions = json_decode($mailbox_settings->access ?? '');
if ($access_permissions && is_array($access_permissions)) {
foreach ($access_permissions as $perm) {
$accessible_route = Mailbox::getAccessPermissionRoute($perm);
if ($accessible_route) {
break;
}
}
}
if (!$accessible_route) {
$accessible_route = \Eventy::filter('mailbox.accessible_settings_route', '', auth()->user(), $mailbox);
}
if ($accessible_route) {
return redirect()->route($accessible_route, ['id' => $mailbox->id]);
} else {
@@ -98,9 +128,17 @@ class MailboxesController extends Controller
}
}
$user = auth()->user();
$mailbox_user = $user->mailboxesWithSettings()->where('mailbox_id', $id)->first();
if (!$mailbox_user && $user->isAdmin()) {
// Admin may not be connected to the mailbox yet
$user->mailboxes()->attach($id);
$mailbox_user = $user->mailboxesWithSettings()->where('mailbox_id', $id)->first();
}
//$mailboxes = Mailbox::all()->except($id);
return view('mailboxes/update', ['mailbox' => $mailbox, 'flashes' => $this->mailboxActiveWarning($mailbox)]);
return view('mailboxes/update', ['mailbox' => $mailbox, 'mailbox_user' => $mailbox_user, 'flashes' => $this->mailboxActiveWarning($mailbox)]);
}
/**
@@ -111,30 +149,63 @@ class MailboxesController extends Controller
*/
public function updateSave($id, Request $request)
{
$invalid = false;
$mailbox = Mailbox::findOrFail($id);
$this->authorize('update', $mailbox);
$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',
'from_name' => 'required|integer',
'from_name_custom' => 'nullable|string|max:128',
'ticket_status' => 'required|integer',
'template' => 'required|integer',
'ticket_assignee' => 'required|integer',
'signature' => 'nullable|string',
]);
//event(new Registered($user = $this->create($request->all())));
if ($validator->fails()) {
return redirect()->route('mailboxes.update', ['id' => $id])
->withErrors($validator)
->withInput();
$user = auth()->user();
if (!$user->can('updateSettings', $mailbox) && !$user->can('updateEmailSignature', $mailbox)) {
\Helper::denyAccess();
}
if ($user->can('updateSettings', $mailbox)) {
// if not admin, the text only fields don't pass so spike them into the request.
if (!auth()->user()->isAdmin()) {
$request->merge([
'name' => $mailbox->name,
'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',
'from_name' => 'required|integer',
'from_name_custom' => 'nullable|string|max:128',
'ticket_status' => 'required|integer',
'template' => 'required|integer',
'ticket_assignee' => 'required|integer',
]);
//event(new Registered($user = $this->create($request->all())));
if (Mailbox::userEmailExists($request->email)) {
$invalid = true;
$validator->errors()->add('email', __('There is a user with such email. Users and mailboxes can not have the same email addresses.'));
}
if ($invalid || $validator->fails()) {
return redirect()->route('mailboxes.update', ['id' => $id])
->withErrors($validator)
->withInput();
}
}
if ($user->can('updateEmailSignature', $mailbox)) {
$validator = Validator::make($request->all(), [
'signature' => 'nullable|string',
]);
if ($validator->fails()) {
return redirect()->route('mailboxes.email_signature', ['id' => $id])
->withErrors($validator)
->withInput();
}
}
\Eventy::action( 'mailboxes.settings_before_save', $id, $request );
$mailbox->fill($request->all());
$mailbox->save();
@@ -151,11 +222,25 @@ class MailboxesController extends Controller
{
$mailbox = Mailbox::findOrFail($id);
$this->authorize('update', $mailbox);
$this->authorize('updatePermissions', $mailbox);
$users = User::where('role', '!=', User::ROLE_ADMIN)->get();
$users = User::nonDeleted()->where('role', '!=', User::ROLE_ADMIN)->get();
$users = User::sortUsers($users);
return view('mailboxes/permissions', ['mailbox' => $mailbox, 'users' => $users, 'mailbox_users' => $mailbox->users]);
$managers = User::nonDeleted()
->select(['users.*', 'mailbox_user.hide', 'mailbox_user.access'])
->leftJoin('mailbox_user', function ($join) use ($mailbox) {
$join->on('mailbox_user.user_id', '=', 'users.id');
$join->where('mailbox_user.mailbox_id', $mailbox->id);
})->get();
$managers = User::sortUsers($managers);
return view('mailboxes/permissions', [
'mailbox' => $mailbox,
'users' => $users,
'managers' => $managers,
'mailbox_users' => $mailbox->users,
]);
}
/**
@@ -167,11 +252,47 @@ class MailboxesController extends Controller
public function permissionsSave($id, Request $request)
{
$mailbox = Mailbox::findOrFail($id);
$this->authorize('update', $mailbox);
$this->authorize('updatePermissions', $mailbox);
$user = auth()->user();
$mailbox->users()->sync($request->users);
$mailbox->syncPersonalFolders($request->users);
// Save admins settings.
$admins = User::nonDeleted()->where('role', User::ROLE_ADMIN)->get();
foreach ($admins as $admin) {
$mailbox_user = $admin->mailboxesWithSettings()->where('mailbox_id', $id)->first();
if (!$mailbox_user) {
// Admin may not be connected to the mailbox yet
$admin->mailboxes()->attach($id);
$mailbox_user = $admin->mailboxesWithSettings()->where('mailbox_id', $id)->first();
}
$mailbox_user->settings->hide = (isset($request->managers[$admin->id]['hide']) ? (int)$request->managers[$admin->id]['hide'] : false);
$mailbox_user->settings->save();
}
// Sets the mailbox_user.access array
$mailbox_users = $mailbox->users;
foreach ($mailbox_users as $mailbox_user) {
$access = [];
$mailbox_with_settings = $mailbox_user->mailboxesWithSettings()->where('mailbox_id', $id)->first();
foreach (\App\Mailbox::$access_permissions as $perm) {
if (!empty($request->managers[$mailbox_user->id]['access'][$perm])) {
$access[] = $request->managers[$mailbox_user->id]['access'][$perm];
}
}
if ($user->id == $mailbox_user->id && !$user->isAdmin()) {
// User with Permission priv's can't edit their own additional priv's.
} else {
$mailbox_with_settings->settings->access = json_encode($access);
}
$mailbox_with_settings->settings->hide = (isset($request->managers[$mailbox_user->id]['hide']) ? (int)$request->managers[$mailbox_user->id]['hide'] : false);
$mailbox_with_settings->settings->save();
}
\Session::flash('flash_success_floating', __('Mailbox permissions saved!'));
return redirect()->route('mailboxes.permissions', ['id' => $id]);
@@ -183,7 +304,7 @@ class MailboxesController extends Controller
public function connectionOutgoing($id)
{
$mailbox = Mailbox::findOrFail($id);
$this->authorize('update', $mailbox);
$this->authorize('admin', $mailbox);
return view('mailboxes/connection', ['mailbox' => $mailbox, 'sendmail_path' => ini_get('sendmail_path'), 'flashes' => $this->mailboxActiveWarning($mailbox)]);
}
@@ -194,14 +315,14 @@ class MailboxesController extends Controller
public function connectionOutgoingSave($id, Request $request)
{
$mailbox = Mailbox::findOrFail($id);
$this->authorize('update', $mailbox);
$this->authorize('admin', $mailbox);
if ($request->out_method == Mailbox::OUT_METHOD_SMTP) {
$validator = Validator::make($request->all(), [
'out_server' => 'required|string|max:255',
'out_port' => 'required|integer',
'out_username' => 'required|string|max:100',
'out_password' => 'required|string|max:255',
'out_username' => 'nullable|string|max:100',
'out_password' => 'nullable|string|max:255',
'out_encryption' => 'required|integer',
]);
@@ -212,9 +333,22 @@ class MailboxesController extends Controller
}
}
$mailbox->fill($request->all());
// Do not save dummy password.
if (preg_match("/^\*+$/", $request->out_password)) {
$params = $request->except(['out_password']);
} else {
$params = $request->all();
}
$mailbox->fill($params);
$mailbox->save();
if (!empty($request->send_test_to)) {
\Option::set('send_test_to', $request->send_test_to);
}
// Sometimes background job continues to use old connection settings.
\Helper::queueWorkRestart();
\Session::flash('flash_success_floating', __('Connection settings saved!'));
return redirect()->route('mailboxes.connection', ['id' => $id]);
@@ -223,12 +357,26 @@ class MailboxesController extends Controller
/**
* Mailbox incoming settings.
*/
public function connectionIncoming($id)
public function connectionIncoming($id, Request $request)
{
$mailbox = Mailbox::findOrFail($id);
$this->authorize('update', $mailbox);
$this->authorize('admin', $mailbox);
return view('mailboxes/connection_incoming', ['mailbox' => $mailbox, 'flashes' => $this->mailboxActiveWarning($mailbox)]);
$fields = [
'in_server' => $mailbox->in_server ?? '',
'in_port' => $mailbox->in_port ?? '',
'in_username' => $mailbox->in_username ?? '',
'in_password' => $mailbox->in_password ?? '',
];
$validator = Validator::make($fields, [
'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);
}
/**
@@ -237,22 +385,45 @@ class MailboxesController extends Controller
public function connectionIncomingSave($id, Request $request)
{
$mailbox = Mailbox::findOrFail($id);
$this->authorize('update', $mailbox);
$this->authorize('admin', $mailbox);
$validator = Validator::make($request->all(), [
'in_server' => 'required|string|max:255',
'in_port' => 'required|integer',
'in_username' => 'required|string|max:100',
'in_password' => 'required|string|max:255',
// $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',
// ]);
// if ($validator->fails()) {
// return redirect()->route('mailboxes.connection.incoming', ['id' => $id])
// ->withErrors($validator)
// ->withInput();
// }
// Checkboxes
$request->merge([
'in_validate_cert' => ($request->filled('in_validate_cert') ?? false),
]);
if ($validator->fails()) {
return redirect()->route('mailboxes.connection.incoming', ['id' => $id])
->withErrors($validator)
->withInput();
// Do not save dummy password.
if (preg_match("/^\*+$/", $request->in_password ?? '')) {
$params = $request->except(['in_password']);
} else {
$params = $request->all();
}
$mailbox->fill($request->all());
$mailbox->fill($params);
// Save IMAP Folders.
// Save all custom folders except INBOX.
$in_imap_folders = [];
if (is_array($request->in_imap_folders)) {
foreach ($request->in_imap_folders as $imap_folder) {
$in_imap_folders[] = $imap_folder;
}
}
$mailbox->setInImapFolders($in_imap_folders);
$mailbox->save();
\Session::flash('flash_success_floating', __('Connection settings saved!'));
@@ -265,10 +436,14 @@ class MailboxesController extends Controller
*/
public function view($id, $folder_id = null)
{
//auth()->guard()->attempt(['email' => '', 'password' => '']);
$user = auth()->user();
$mailbox = Mailbox::findOrFail($id);
$this->authorize('view', $mailbox);
if ($user->isAdmin()) {
$mailbox = Mailbox::findOrFailWithSettings($id, $user->id);
} else {
$mailbox = Mailbox::findOrFail($id);
}
$this->authorize('viewCached', $mailbox);
$folders = $mailbox->getAssesibleFolders();
@@ -287,8 +462,6 @@ class MailboxesController extends Controller
$this->authorize('view', $folder);
$user = auth()->user();
$query_conversations = Conversation::getQueryByFolder($folder, $user->id);
$conversations = $folder->queryAddOrderBy($query_conversations)->paginate(Conversation::DEFAULT_LIST_SIZE);
@@ -304,7 +477,7 @@ class MailboxesController extends Controller
{
$flashes = [];
if ($mailbox) {
if ($mailbox && \Auth::user()->can('admin', $mailbox)) {
if (Route::currentRouteName() != 'mailboxes.connection' && !$mailbox->isOutActive()) {
$flashes[] = [
'type' => 'warning',
@@ -330,14 +503,14 @@ class MailboxesController extends Controller
public function autoReply($id)
{
$mailbox = Mailbox::findOrFail($id);
$this->authorize('update', $mailbox);
$this->authorize('updateAutoReply', $mailbox);
if (!$mailbox->auto_reply_subject) {
$mailbox->auto_reply_subject = 'Re: {%subject%}';
}
return view('mailboxes/auto_reply', [
'mailbox' => $mailbox
'mailbox' => $mailbox,
]);
}
@@ -348,10 +521,11 @@ class MailboxesController extends Controller
{
$mailbox = Mailbox::findOrFail($id);
$this->authorize('update', $mailbox);
// $this->authorize('update', $mailbox);
$this->authorize('updateAutoReply', $mailbox);
$request->merge([
'auto_reply_enabled' => ($request->filled('auto_reply_enabled') ?? false)
'auto_reply_enabled' => ($request->filled('auto_reply_enabled') ?? false),
]);
if ($request->auto_reply_enabled) {
@@ -363,8 +537,8 @@ class MailboxesController extends Controller
]);
$validator->setAttributeNames([
'auto_reply_subject' => __('Subject'),
'auto_reply_message' => __('Message')
]);
'auto_reply_message' => __('Message'),
]);
if ($validator->fails()) {
return redirect()->route('mailboxes.auto_reply', ['id' => $id])
@@ -382,6 +556,24 @@ class MailboxesController extends Controller
return redirect()->route('mailboxes.auto_reply', ['id' => $id]);
}
/**
* Auto reply settings.
*/
public function emailSignature($id)
{
$mailbox = Mailbox::findOrFail($id);
$this->authorize('updateAutoReply', $mailbox);
if (!$mailbox->auto_reply_subject) {
$mailbox->auto_reply_subject = 'Re: {%subject%}';
}
return view('mailboxes/email_signature', [
'mailbox' => $mailbox,
]);
}
/**
* Users ajax controller.
*/
@@ -402,12 +594,20 @@ class MailboxesController extends Controller
if (!$mailbox) {
$response['msg'] = __('Mailbox not found');
} elseif (!$user->can('update', $mailbox)) {
} elseif (!$user->can('admin', $mailbox)) {
$response['msg'] = __('Not enough permissions');
} elseif (empty($request->to)) {
$response['msg'] = __('Please specify recipient of the test email');
}
// 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>']);
}
}
if (!$response['msg']) {
$test_result = false;
@@ -418,7 +618,7 @@ class MailboxesController extends Controller
}
if (!$test_result && !$response['msg']) {
$response['msg'] = __('Error occurend sending email. Please check your mail server logs for more details.');
$response['msg'] = __('Error occurred sending email. Please check your mail server logs for more details.');
}
}
@@ -437,22 +637,86 @@ class MailboxesController extends Controller
$mailbox = Mailbox::find($request->mailbox_id);
if (!$mailbox) {
$response['msg'] = __('Conversation not found');
} elseif (!$user->can('update', $mailbox)) {
$response['msg'] = __('Mailbox not found');
} elseif (!$user->can('admin', $mailbox)) {
$response['msg'] = __('Not enough permissions');
}
if (!$response['msg']) {
$response = \Eventy::filter('mailbox.fetch_test', $response, $mailbox);
$tested = (isset($response['tested']) && $response['tested'] === true);
// 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>']);
}
}
if (!$response['msg'] && !$tested) {
$test_result = false;
try {
$test_result = \App\Misc\Mail::fetchTest($mailbox);
$test_result = \MailHelper::fetchTest($mailbox);
} catch (\Exception $e) {
$response['msg'] = $e->getMessage();
}
if (!$test_result && !$response['msg']) {
$response['msg'] = __('Error occurend connecting to the server');
$response['msg'] = __('Error occurred connecting to the server');
}
}
if (!$response['msg'] && !$tested) {
$response['status'] = 'success';
}
break;
// Retrieve a list of available IMAP folders from server.
case 'imap_folders':
$mailbox = Mailbox::find($request->mailbox_id);
if (!$mailbox) {
$response['msg'] = __('Mailbox not found');
} elseif (!$user->can('admin', $mailbox)) {
$response['msg'] = __('Not enough permissions');
}
$response['folders'] = [];
if (!$response['msg']) {
try {
$client = \MailHelper::getMailboxClient($mailbox);
$client->connect();
$imap_folders = $client->getFolders();
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) {
if (!empty($child_imap_folder->fullName)) {
$response['folders'][] = $child_imap_folder->fullName;
}
}
}
}
}
if (count($response['folders'])) {
$response['msg_success'] = __('IMAP folders retrieved: '.implode(', ', $response['folders']));
} else {
$response['msg_success'] = __('Connected, but no IMAP folders found');
}
} catch (\Exception $e) {
$response['msg'] = $e->getMessage();
}
}
@@ -467,7 +731,7 @@ class MailboxesController extends Controller
if (!$mailbox) {
$response['msg'] = __('Mailbox not found');
} elseif (!$user->can('update', $mailbox)) {
} elseif (!$user->can('admin', $mailbox)) {
$response['msg'] = __('Not enough permissions');
} elseif (!Hash::check($request->password, $user->password)) {
$response['msg'] = __('Please double check your password, and try again');
@@ -482,7 +746,7 @@ class MailboxesController extends Controller
$mailbox->users()->sync([]);
$mailbox->folders()->delete();
// Maybe remove notifications on events in this mailbox?
$mailbox->delete();
\Session::flash('flash_success_floating', __('Mailbox deleted'));
@@ -491,6 +755,31 @@ class MailboxesController extends Controller
}
break;
// Mute notifications
case 'mute':
$mailbox = Mailbox::find($request->mailbox_id);
if (!$mailbox) {
$response['msg'] = __('Mailbox not found');
} elseif (!$user->isAdmin()) {
$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
$user->mailboxes()->attach($id);
$mailbox_user = $user->mailboxesWithSettings()->where('mailbox_id', $mailbox->id)->first();
}
$mailbox_user->settings->mute = (bool)$request->mute;
$mailbox_user->settings->save();
$response['status'] = 'success';
}
break;
default:
$response['msg'] = 'Unknown action';
break;
@@ -502,4 +791,107 @@ class MailboxesController extends Controller
return \Response::json($response);
}
public function oauth(Request $request)
{
$mailbox_id = $request->id ?? '';
$provider = $request->provider ?? '';
$state_data = [];
if (!empty($request->state)) {
$state_data = json_decode($request->state, true);
if (!empty($state_data['mailbox_id'])) {
$mailbox_id = $state_data['mailbox_id'];
}
if (!empty($state_data['provider'])) {
$provider = $state_data['provider'];
}
}
// MS Exchange.
if (!empty($request->error) && $request->error == 'invalid_request' && !empty($request->error_description)) {
return htmlspecialchars($request->error_description);
}
if (empty($provider)) {
return 'Invalid oAuth Provider';
}
$mailbox = Mailbox::findOrFail($mailbox_id);
$this->authorize('admin', $mailbox);
if (empty($mailbox)) {
return __('Mailbox not found').': '.$mailbox_id;
}
if (empty($mailbox->in_username)) {
return 'Enter oAuth Client ID as Username and save mailbox settings';
}
if (empty($mailbox->in_password)) {
return 'Enter oAuth Client Secret as Password and save mailbox settings';
}
$session_data = [];
if (\Session::get('mailbox_oauth_'.$provider.'_'.$mailbox_id)) {
$session_data = \Session::get('mailbox_oauth_'.$provider.'_'.$mailbox_id);
}
if (empty($request->code)) {
$state = [
'provider' => $provider,
'mailbox_id' => $mailbox_id,
'state' => crc32($mailbox->in_username.$mailbox->in_password),
];
$url = \MailHelper::oauthGetAuthorizationUrl(\MailHelper::OAUTH_PROVIDER_MICROSOFT, [
'state' => json_encode($state),
'client_id' => $mailbox->in_username,
]);
if ($url) {
\Session::put('mailbox_oauth_'.$provider.'_'.$mailbox_id, $state);
// [
// 'provider' => $request->provider,
// 'mailbox_id' => $request->mailbox_id,
// 'state' => $provider->getState(),
// ]);
return redirect()->away($url);
} else {
return 'Could not generate authorization URL: check Client ID (Username) and Client Secret (Password)';
}
// Check given state against previously stored one to mitigate CSRF attack
} elseif (empty($request->state) || ($state_data['state'] ?? '') !== ($session_data['state'] ?? '')) {
\Session::forget('mailbox_oauth_'.$provider.'_'.$mailbox_id);
return 'Invalid oAuth state';
} else {
// 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,
'code' => $request->code,
]);
if (!empty($token_data['a_token'])) {
$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]);
}
}
public function oauthDisconnect(Request $request)
{
$mailbox_id = $request->id ?? '';
$provider = $request->provider ?? '';
$mailbox = Mailbox::findOrFail($mailbox_id);
$this->authorize('admin', $mailbox);
// oAuth Disconnect.
$mailbox->removeMetaParam('oauth', true);
return \MailHelper::oauthDisconnect($provider, route('mailboxes.connection.incoming', ['id' => $mailbox_id]));
}
}

View File

@@ -28,8 +28,10 @@ class ModulesController extends Controller
{
$installed_modules = [];
$modules_directory = [];
$all_modules = [];
$flashes = [];
$updates_available = false;
$flash = \Cache::get('modules_flash');
if ($flash) {
$flashes[] = $flash;
@@ -52,21 +54,25 @@ class ModulesController extends Controller
\Module::clearCache();
$modules = \Module::all();
foreach ($modules as $module) {
$img = '';
$installed_modules[] = [
'alias' => $module->getAlias(),
'name' => $module->getName(),
'description' => $module->getDescription(),
'version' => $module->get('version'),
'detailsUrl' => $module->get('detailsUrl'),
'author' => $module->get('author'),
'authorUrl' => $module->get('authorUrl'),
'requiredAppVersion' => $module->get('requiredAppVersion'),
'img' => $img,
'active' => $module->active(), //\App\Module::isActive($module->getAlias()),
'installed' => true,
'activated' => \App\Module::isLicenseActivated($module->getAlias(), $module->get('authorUrl')),
'license' => \App\Module::getLicense($module->getAlias()),
'alias' => $module->getAlias(),
'name' => $module->getName(),
'description' => $module->getDescription(),
'version' => $module->get('version'),
'detailsUrl' => $module->get('detailsUrl'),
'author' => $module->get('author'),
'authorUrl' => $module->get('authorUrl'),
'requiredAppVersion' => $module->get('requiredAppVersion'),
'requiredPhpExtensions' => $module->get('requiredPhpExtensions'),
'requiredPhpExtensionsMissing' => \App\Module::getMissingExtensions($module->get('requiredPhpExtensions')),
'requiredModulesMissing' => \App\Module::getMissingModules($module->get('requiredModules'), $modules),
'img' => $module->get('img'),
'active' => $module->active(), //\App\Module::isActive($module->getAlias()),
'installed' => true,
'activated' => \App\Module::isLicenseActivated($module->getAlias(), $module->get('authorUrl')),
'license' => \App\Module::getLicense($module->getAlias()),
// Determined later
'new_version' => '',
];
}
@@ -79,12 +85,24 @@ class ModulesController extends Controller
// Prepare directory modules
if (is_array($modules_directory)) {
foreach ($modules_directory as $i_dir => $dir_module) {
// Remove installed modules from modules directory
// 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'];
// Remove installed modules from modules directory.
unset($modules_directory[$i_dir]);
// Detect if new version is available
if (!empty($dir_module['version']) && version_compare($dir_module['version'], $module['version'], '>')) {
$installed_modules[$i_installed]['new_version'] = $dir_module['version'];
$updates_available = true;
}
continue 2;
}
}
@@ -94,6 +112,9 @@ class ModulesController extends Controller
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;
}
@@ -104,7 +125,9 @@ class ModulesController extends Controller
return view('modules/modules', [
'installed_modules' => $installed_modules,
'modules_directory' => $modules_directory,
'flashes' => $flashes
'flashes' => $flashes,
'updates_available' => $updates_available,
'all_modules' => $all_modules,
]);
}
@@ -123,7 +146,7 @@ class ModulesController extends Controller
case 'install':
case 'activate_license':
$license = $request->license;
$alias = $request->alias;
$alias = $request->alias;
if (!$license) {
$response['msg'] = __('Empty license key');
@@ -133,31 +156,31 @@ class ModulesController extends Controller
$params = [
'license' => $license,
'module_alias' => $alias,
'url' => $request->getHttpHost(),
'url' => \App\Module::getAppUrl(),
];
$result = WpApi::activateLicense($params);
if (WpApi::$lastError) {
$response['msg'] = WpApi::$lastError['message'];
} else if (!empty($result['code']) && !empty($result['message'])) {
} elseif (!empty($result['code']) && !empty($result['message'])) {
$response['msg'] = $result['message'];
} else {
if (!empty($result['status']) && $result['status'] == 'valid') {
if ($request->action == 'install') {
// Download and install module
$license_details = WpApi::getVersion($params);
if (WpApi::$lastError) {
$response['msg'] = WpApi::$lastError['message'];
} else if (!empty($license_details['code']) && !empty($license_details['message'])) {
} elseif (!empty($license_details['code']) && !empty($license_details['message'])) {
$response['msg'] = $license_details['message'];
} 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) {
} catch (\Exception $e) {
$response['msg'] = $e->getMessage();
}
@@ -168,7 +191,7 @@ class ModulesController extends Controller
// Extract
try {
\Helper::unzip($module_archive, \Module::getPath());
} catch(\Exception $e) {
} catch (\Exception $e) {
$response['msg'] = $e->getMessage();
}
// Check if extracted module exists
@@ -190,9 +213,7 @@ class ModulesController extends Controller
\Session::flash('flash_success_floating', __('Module successfully installed!'));
$response['status'] = 'success';
} elseif ($download_error) {
$response['reload'] = true;
if ($response['msg']) {
@@ -202,7 +223,7 @@ class ModulesController extends Controller
\Session::flash('flash_error_unescaped', __('Error occured 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>']));
}
} else {
$response['msg'] = __('Error occured, please try again later.');
$response['msg'] = __('Error occured. Please try again later.');
}
} else {
// Just activate license
@@ -211,36 +232,10 @@ class ModulesController extends Controller
\Session::flash('flash_success_floating', __('License successfully activated!'));
$response['status'] = 'success';
}
} elseif (!empty($result['error'])) {
switch ($result['error']) {
case 'missing':
$response['msg'] = __('License key does not exist');
break;
case 'license_not_activable':
$response['msg'] = __("You have to activate each bundle's module separately");
break;
case 'disabled':
$response['msg'] = __("License key has been revoked");
break;
case 'no_activations_left':
$response['msg'] = __("No activations left for this license key");
break;
case 'expired':
$response['msg'] = __("License key has expired");
break;
case 'key_mismatch':
$response['msg'] = __("License key belongs to another module");
break;
case 'invalid_item_id':
$response['msg'] = __("Module not found in the modules directory");
break;
default:
$response['msg'] = __('Error code:'.' '.$result['error']);
break;
}
$response['msg'] = $this->getErrorMessage($result['error'], $result);
} else {
$response['msg'] = __('Error occured, please try again later.');
$response['msg'] = __('Error occured. Please try again later.');
}
}
}
@@ -248,27 +243,126 @@ class ModulesController extends Controller
case 'activate':
$alias = $request->alias;
\App\Module::setActive($alias, true);
$module = \Module::findByAlias($alias);
$outputLog = new BufferedOutput;
\Artisan::call('freescout:module-install', ['module_alias' => $alias], $outputLog);
if (!$module) {
$response['msg'] = __('Module not found').': '.$alias;
}
// Check license
if (!$response['msg']) {
if (!empty($module->get('authorUrl')) && $module->isOfficial()) {
$params = [
'license' => $module->getLicense(),
'module_alias' => $alias,
'url' => \App\Module::getAppUrl(),
];
$license_result = WpApi::checkLicense($params);
if (!empty($license_result['code']) && !empty($license_result['message'])) {
// Remove remembered license key and deactivate license in DB
\App\Module::deactivateLicense($alias, '');
$response['msg'] = $license_result['message'];
} elseif (!empty($license_result['status']) && $license_result['status'] != 'valid' && $license_result['status'] != 'inactive') {
// Remove remembered license key and deactivate license in DB
\App\Module::deactivateLicense($alias, '');
switch ($license_result['status']) {
case 'expired':
$response['msg'] = __('License key has expired');
break;
case 'disabled':
$response['msg'] = __('License key has been revoked');
break;
case 'inactive':
$response['msg'] = __('License key has not been activated yet');
case 'site_inactive':
$response['msg'] = __('Your domain is deactivated');
break;
}
}
}
}
if (!$response['msg']) {
\App\Module::setActive($alias, true);
$outputLog = new BufferedOutput();
\Artisan::call('freescout:module-install', ['module_alias' => $alias], $outputLog);
$output = $outputLog->fetch();
// Get module name
$name = '?';
if ($module) {
$name = $module->getName();
}
$type = 'danger';
$msg = __('Error occured 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')) {
$type = 'success';
$msg = __('":name" module successfully activated!', ['name' => $name]);
} else {
// Deactivate the module.
\App\Module::setActive($alias, false);
\Artisan::call('freescout:clear-cache');
}
// Check public folder.
if ($module && file_exists($module->getPath().DIRECTORY_SEPARATOR.'Public')) {
$public_folder = public_path().\Module::getPublicPath($alias);
if (!file_exists($public_folder)) {
$type = 'danger';
$msg = 'Error occured creating a module symlink ('.$public_folder.'). Please check folder permissions.';
\App\Module::setActive($alias, false);
\Artisan::call('freescout:clear-cache');
}
}
if ($type == 'success') {
// Migrate again, in case migration did not work in the moment the module was activated.
\Artisan::call('migrate', ['--force' => true]);
}
// \Session::flash does not work after BufferedOutput
$flash = [
'text' => '<strong>'.$msg.'</strong><pre class="margin-top">'.$output.'</pre>',
'unescaped' => true,
'type' => $type,
];
\Cache::forever('modules_flash', $flash);
$response['status'] = 'success';
}
break;
case 'deactivate':
$alias = $request->alias;
\App\Module::setActive($alias, false);
$outputLog = new BufferedOutput();
\Artisan::call('freescout:clear-cache', [], $outputLog);
$output = $outputLog->fetch();
// Get module name
$module = \Module::findByAlias($alias);
$name = '?';
if ($module) {
$name = $module->getName();
}
$type = 'danger';
$msg = __('Error occured activating module');
if (session('flashes_floating') && is_array(session('flashes_floating'))) {
// If there was any error, module has been deactivate via modules.register_error filter
$msg = '';
foreach (session('flashes_floating') as $flash) {
$msg .= $flash['text'].' ';
}
} elseif (strstr($output, 'Configuration cached successfully')) {
$msg = __('Error occured deactivating :name module', ['name' => $name]);
if (strstr($output, 'Configuration cached successfully')) {
$type = 'success';
$msg = __('Module successfully activated!');
} else {
// Deactivate module
\App\Module::setActive($alias, false);
\Artisan::call('freescout:clear-cache');
$msg = __('":name" module successfully DEactivated!', ['name' => $name]);
}
// \Session::flash does not work after BufferedOutput
@@ -281,29 +375,54 @@ class ModulesController extends Controller
$response['status'] = 'success';
break;
case 'deactivate':
case 'deactivate_license':
$license = $request->license;
$alias = $request->alias;
\App\Module::setActive($alias, false);
$outputLog = new BufferedOutput;
\Artisan::call('freescout:clear-cache', [], $outputLog);
$output = $outputLog->fetch();
$type = 'danger';
$msg = __('Error occured deactivating module');
if (strstr($output, 'Configuration cached successfully')) {
$type = 'success';
$msg = __('Module successfully deactivated!');
if (!$license) {
$response['msg'] = __('Empty license key');
}
// \Session::flash does not work after BufferedOutput
$flash = [
'text' => '<strong>'.$msg.'</strong><pre class="margin-top">'.$output.'</pre>',
'unescaped' => true,
'type' => $type,
];
\Cache::forever('modules_flash', $flash);
$response['status'] = 'success';
if (!$response['msg']) {
$params = [
'license' => $license,
'module_alias' => $alias,
'url' => (!empty($request->any_url) ? '*' : \App\Module::getAppUrl()),
];
$result = WpApi::deactivateLicense($params);
if (WpApi::$lastError) {
$response['msg'] = WpApi::$lastError['message'];
} elseif (!empty($result['code']) && !empty($result['message'])) {
$response['msg'] = $result['message'];
} else {
if (!empty($result['status']) && $result['status'] == 'success') {
$db_module = \App\Module::getByAlias($alias);
if ($db_module && trim($db_module->license) == trim($license)) {
// Remove remembered license key and deactivate license in DB
\App\Module::deactivateLicense($alias, '');
// Deactivate module
\App\Module::setActive($alias, false);
\Artisan::call('freescout:clear-cache', []);
}
// Flash does not work here.
$flash = [
'text' => '<strong>'.__('License successfully DEactivated!').'</strong>',
'unescaped' => true,
'type' => 'success',
];
\Cache::forever('modules_flash', $flash);
$response['status'] = 'success';
} elseif (!empty($result['error'])) {
$response['msg'] = $this->getErrorMessage($result['error'], $result);
} else {
$response['msg'] = __('Error occured. Please try again later.');
}
}
}
break;
case 'delete':
@@ -318,12 +437,126 @@ class ModulesController extends Controller
$module->delete();
\Session::flash('flash_success_floating', __('Module deleted'));
} else {
$response['msg'] = __("Module not found").': '.$alias;
$response['msg'] = __('Module not found').': '.$alias;
}
$response['status'] = 'success';
break;
case 'update':
$alias = $request->alias;
$module = \Module::findByAlias($alias);
if (!$module) {
$response['msg'] = __('Module not found').': '.$alias;
}
// 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 (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 {
\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 occured 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 occured. Please try again later.');
}
}
// 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();
}
$type = 'danger';
$msg = __('Error occured 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')) {
$type = 'success';
$msg = __('":name" module successfully updated!', ['name' => $name]);
} else {
// Deactivate module
\App\Module::setActive($alias, false);
\Artisan::call('freescout:clear-cache');
}
// \Session::flash does not work after BufferedOutput
$flash = [
'text' => '<strong>'.$msg.'</strong><pre class="margin-top">'.$output.'</pre>',
'unescaped' => true,
'type' => $type,
];
\Cache::forever('modules_flash', $flash);
$response['status'] = 'success';
}
break;
default:
$response['msg'] = 'Unknown action';
break;
@@ -335,4 +568,40 @@ 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

@@ -2,7 +2,10 @@
namespace App\Http\Controllers;
use App\Attachment;
use App\Conversation;
use App\Option;
use App\Thread;
use App\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
@@ -17,7 +20,6 @@ class PublicController extends Controller
*/
public function __construct()
{
}
/**
@@ -32,12 +34,15 @@ class PublicController extends Controller
}
$user = User::where('invite_hash', $hash)->first();
if ($user && $user->locale) {
\Helper::setLocale($user->locale);
}
return view('public/user_setup', ['user' => $user]);
}
/**
* Save user from invitation.
*
*/
public function userSetupSave($hash, Request $request)
{
@@ -106,4 +111,95 @@ class PublicController extends Controller
return redirect()->route('dashboard');
}
/*
* Set a thread as read by customer
*/
public function setThreadAsRead($conversation_id, $thread_id)
{
$conversation = Conversation::findOrFail($conversation_id);
$thread = Thread::findOrFail($thread_id);
// We only track the first opening
if (empty($thread->opened_at)) {
$thread->opened_at = date('Y-m-d H:i:s');
$thread->save();
\Eventy::action('thread.opened', $thread, $conversation);
}
// 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);
$response->header('Content-type', 'image/gif');
$response->header('Content-Length', 42);
$response->header('Cache-Control', 'private, no-cache, no-cache=Set-Cookie, proxy-revalidate');
$response->header('Expires', 'Wed, 11 Jan 2000 12:59:00 GMT');
$response->header('Last-Modified', 'Wed, 11 Jan 2006 12:59:00 GMT');
$response->header('Pragma', 'no-cache');
return $response;
}
/**
* Download an attachment.
*/
public function downloadAttachment($dir_1, $dir_2, $dir_3, $file_name, Request $request)
{
$id = $request->query('id', '');
$token = $request->query('token', '');
$attachment = null;
// Old attachments can not be requested by id.
if (!$token && $id) {
return \Helper::denyAccess();
}
// Get attachment by id.
if ($id) {
$attachment = Attachment::findOrFail($id);
}
if (!$attachment) {
$attachment = Attachment::where('file_dir', $dir_1.DIRECTORY_SEPARATOR.$dir_2.DIRECTORY_SEPARATOR.$dir_3.DIRECTORY_SEPARATOR)
->where('file_name', $file_name)
->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) {
return \Helper::denyAccess();
}
$view_attachment = false;
$file_ext = strtolower(pathinfo($attachment->file_name, PATHINFO_EXTENSION));
// Some file type should be viewed in the browser instead of downloading.
if (in_array($file_ext, config('app.viewable_attachments'))) {
$view_attachment = true;
}
if (config('app.download_attachments_via') == 'apache') {
// Send using Apache mod_xsendfile.
$response = response(null)
->header('Content-Type' , $attachment->mime_type)
->header('X-Sendfile', $attachment->getLocalFilePath());
if (!$view_attachment) {
$response->header('Content-Disposition', 'attachment; filename="'.$attachment->file_name.'"');
}
} elseif (config('app.download_attachments_via') == 'nginx') {
// Send using Nginx.
$response = response(null)
->header('Content-Type' , $attachment->mime_type)
->header('X-Accel-Redirect', $attachment->getLocalFilePath(false));
if (!$view_attachment) {
$response->header('Content-Disposition', 'attachment; filename="'.$attachment->file_name.'"');
}
} else {
$response = $attachment->download($view_attachment);
}
return $response;
}
}

View File

@@ -3,11 +3,10 @@
namespace App\Http\Controllers;
use App\ActivityLog;
use App\Option;
use App\Misc\Helper;
use App\SendLog;
use App\Thread;
use App\User;
use Carbon\Carbon;
use Illuminate\Http\Request;
class SecureController extends Controller
@@ -29,7 +28,15 @@ class SecureController extends Controller
*/
public function dashboard()
{
$mailboxes = auth()->user()->mailboxesCanView();
$user = auth()->user();
if (!$user->isAdmin()) {
$mailboxes = $user->mailboxesCanView();
} else {
$mailboxes = $user->mailboxesCanViewWithSettings();
}
// Sort by name.
$mailboxes = \Eventy::filter('dashboard.mailboxes', $mailboxes->sortBy('name'));
return view('secure/dashboard', ['mailboxes' => $mailboxes]);
}
@@ -63,7 +70,7 @@ class SecureController extends Controller
$activities = ActivityLog::inLog($request->name)->orderBy('created_at', 'desc')->paginate($page_size);
$name = $request->name;
} elseif (count($names)) {
$name = ActivityLog::NAME_OUT_EMAILS;
$name = ActivityLog::NAME_OUT_EMAILS;
// $activities = ActivityLog::inLog($names[0])->orderBy('created_at', 'desc')->paginate($page_size);
// $name = $names[0];
}
@@ -113,15 +120,17 @@ class SecureController extends Controller
$activities = SendLog::orderBy('created_at', 'desc')->paginate($page_size);
foreach ($activities as $record) {
$conversation = '';
if ($record->thread_id) {
$conversation = Thread::find($record->thread_id);
}
$status = $record->getStatusName();
if ($record->status_message) {
$status .= '. '.$record->status_message;
if ($record->status == SendLog::STATUS_SEND_ERROR) {
$status .= '. Message-ID: '.$record->message_id;
}
}
$logs[] = [
@@ -134,7 +143,6 @@ class SecureController extends Controller
'customer' => $record->customer,
];
}
}
array_unshift($names, ActivityLog::NAME_OUT_EMAILS);
@@ -145,16 +153,16 @@ class SecureController extends Controller
}
return view('secure/logs', [
'logs' => $logs,
'names' => $names,
'logs' => $logs,
'names' => $names,
'current_name' => $name,
'cols' => $cols,
'activities' => $activities
'cols' => $cols,
'activities' => $activities,
]);
}
/**
* Logs page submitted
* Logs page submitted.
*/
public function logsSubmit(Request $request)
{
@@ -165,7 +173,7 @@ class SecureController extends Controller
$activities = ActivityLog::inLog($request->name)->orderBy('created_at', 'desc')->get();
$name = $request->name;
} elseif (count($names = ActivityLog::select('log_name')->distinct()->get()->pluck('log_name'))) {
$name = ActivityLog::NAME_OUT_EMAILS;
$name = ActivityLog::NAME_OUT_EMAILS;
// $activities = ActivityLog::inLog($names[0])->orderBy('created_at', 'desc')->get();
// $name = $names[0];
}
@@ -181,4 +189,41 @@ class SecureController extends Controller
return redirect()->route('logs', ['name' => $name]);
}
/**
* Upload files and images.
*/
public function upload(Request $request, $allowed_exts = [])
{
// 'jpg','gif','png'
$response = [
'status' => 'error',
'msg' => '', // this is error message
];
$user = auth()->user();
if (!$user) {
$response['msg'] = __('Please login to upload file');
}
if (!$request->hasFile('file') || !$request->file('file')->isValid() || !$request->file) {
$response['msg'] = __('Error occured uploading file');
}
if (!$response['msg']) {
$upload = Helper::uploadFile($request->file, $allowed_exts);
$filename = basename($upload);
if ($upload) {
$response['status'] = 'success';
$response['url'] = Helper::uploadedFileUrl($filename);
} else {
$response['msg'] = __('Error occured uploading file');
}
}
return \Response::json($response);
}
}

View File

@@ -2,10 +2,11 @@
namespace App\Http\Controllers;
use App\Conversation;
use App\Option;
use App\Subscription;
use App\User;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;
use Validator;
class SettingsController extends Controller
@@ -21,12 +22,12 @@ class SettingsController extends Controller
}
/**
* General settings
* General settings.
*
* @return \Illuminate\Http\Response
*/
public function view($section = 'general')
{
{
$settings = $this->getSectionSettings($section);
if (!$settings) {
@@ -36,10 +37,10 @@ class SettingsController extends Controller
$sections = $this->getSections();
$template_vars = [
'settings' => $settings,
'section' => $section,
'sections' => $this->getSections(),
'section_name' => $sections[$section]['title']
'settings' => $settings,
'section' => $section,
'sections' => $this->getSections(),
'section_name' => $sections[$section]['title'],
];
$template_vars = $this->getTemplateVars($section, $template_vars);
@@ -48,78 +49,184 @@ class SettingsController extends Controller
public function getValidator($section)
{
switch ($section) {
case 'emails':
$rules = [
'settings.mail_from' => 'required|email',
];
break;
// default:
// $rules = \Event::fire('filter.settings_validate_rules');
// break;
}
$rules = $this->getSectionParams($section, 'validator_rules');
if (!empty($rules)) {
return Validator::make(request()->all(), $rules);
}
return null;
}
public function getTemplateVars($section, $template_vars)
{
$section_vars = $this->getSectionParams($section, 'template_vars');
if ($section_vars && is_array($section_vars)) {
return array_merge($template_vars, $section_vars);
} else {
return $template_vars;
}
}
/**
* Parameters of the sections settings.
*
* If in settings parameter `env` is set, option will be saved into .env file
* instead of DB.
*
* @param [type] $section [description]
* @param string $param [description]
*
* @return [type] [description]
*/
public function getSectionParams($section, $param = '')
{
$params = [];
switch ($section) {
case 'emails':
$template_vars['sendmail_path'] = ini_get('sendmail_path');
$template_vars['mail_drivers'] = [
'mail' => __("PHP's mail() function"),
'sendmail' => __("Sendmail"),
'smtp' => 'SMTP',
$params = [
'template_vars' => [
'sendmail_path' => ini_get('sendmail_path'),
'mail_drivers' => [
'mail' => __("PHP's mail() function"),
'sendmail' => __('Sendmail'),
'smtp' => 'SMTP',
],
],
'validator_rules' => [
'settings.mail_from' => 'required|email',
],
'settings' => [
'fetch_schedule' => [
'env' => 'APP_FETCH_SCHEDULE',
],
'mail_password' => [
'safe_password' => true,
'encrypt' => true,
]
],
];
break;
// default:
// $template_vars = \Event::fire('filter.settings_template_vars', [$template_vars]);
// break;
case 'general':
$params = [
'settings' => [
'email_conv_history' => [
'env' => 'APP_EMAIL_CONV_HISTORY',
],
'email_user_history' => [
'env' => 'APP_EMAIL_USER_HISTORY',
],
'locale' => [
'env' => 'APP_LOCALE',
],
'timezone' => [
'env' => 'APP_TIMEZONE',
],
'user_permissions' => [
'env' => 'APP_USER_PERMISSIONS',
'env_encode' => true,
],
],
];
break;
case 'alerts':
$subscriptions_defaults = Subscription::getDefaultSubscriptions();
$subscriptions = array();
foreach ($subscriptions_defaults as $medium => $subscriptions_for_medium) {
foreach ($subscriptions_defaults[$medium] as $subscription) {
$subscriptions[] = (object) array("medium" => $medium, "event" => $subscription);
}
}
$params = [
'template_vars' => [
'logs' => \App\ActivityLog::getAvailableLogs(),
'person' => null,
'subscriptions' => $subscriptions,
'mobile_available' => \Eventy::filter('notifications.mobile_available', false),
],
'settings' => [
'alert_logs' => [
'env' => 'APP_ALERT_LOGS',
],
'alert_logs_period' => [
'env' => 'APP_ALERT_LOGS_PERIOD',
],
],
];
// todo: monitor App Logs
foreach ($params['template_vars']['logs'] as $i => $log) {
if ($log == \App\ActivityLog::NAME_APP_LOGS || $log == \App\ActivityLog::NAME_OUT_EMAILS) {
unset($params['template_vars']['logs'][$i]);
}
}
break;
default:
$params = \Eventy::filter('settings.section_params', $params, $section);
break;
}
return $template_vars;
if ($param) {
if (isset($params[$param])) {
return $params[$param];
} else {
return;
}
} else {
return $params;
}
}
public function getSectionSettings($section)
{
$settings = [];
switch ($section) {
case 'general':
$settings = [
'company_name' => Option::get('company_name', \Config::get('app.name')),
'next_ticket' => Option::get('next_ticket'),
'user_permissions' => Option::get('user_permissions', []),
'next_ticket' => (Option::get('next_ticket') >= Conversation::max('number') + 1) ? Option::get('next_ticket') : Conversation::max('number') + 1,
'user_permissions' => User::getGlobalUserPermissions(),
'email_branding' => Option::get('email_branding'),
'open_tracking' => Option::get('open_tracking'),
'email_conv_history' => config('app.email_conv_history'),
'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),
'locale' => \Helper::getRealAppLocale(),
'timezone' => config('app.timezone'),
];
break;
case 'emails':
$settings = [
'mail_from' => \App\Misc\Mail::getSystemMailFrom(),
'mail_driver' => Option::get('mail_driver', \Config::get('mail.driver')),
'mail_host' => Option::get('mail_host', \Config::get('mail.host')),
'mail_port' => Option::get('mail_port', \Config::get('mail.port')),
'mail_username' => Option::get('mail_username', \Config::get('mail.username')),
'mail_password' => Option::get('mail_password', \Config::get('mail.password')),
'mail_from' => \App\Misc\Mail::getSystemMailFrom(),
'mail_driver' => Option::get('mail_driver', \Config::get('mail.driver')),
'mail_host' => Option::get('mail_host', \Config::get('mail.host')),
'mail_port' => Option::get('mail_port', \Config::get('mail.port')),
'mail_username' => Option::get('mail_username', \Config::get('mail.username')),
'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'),
];
break;
case 'alerts':
$settings = [
'alert_recipients' => Option::get('alert_recipients'),
'alert_fetch' => Option::get('alert_fetch'),
'alert_fetch_period' => Option::get('alert_fetch_period'),
'alert_send' => Option::get('alert_send'),
];
$settings = Option::getOptions([
'alert_recipients',
'alert_fetch',
'alert_fetch_period',
'alert_logs',
'alert_logs_names',
'alert_logs_period',
'subscription_defaults',
], [
'alert_logs_names' => [],
'alert_logs' => config('app.alert_logs'),
'alert_logs_period' => config('app.alert_logs_period'),
]);
break;
default:
$settings = \Event::fire('filter.section_settings', [$section]);
$settings = \Eventy::filter('settings.section_settings', $settings, $section);
break;
}
@@ -134,7 +241,8 @@ class SettingsController extends Controller
'emails' => ['title' => __('Mail Settings'), 'icon' => 'transfer', 'order' => 200],
'alerts' => ['title' => __('Alerts'), 'icon' => 'bell', 'order' => 300],
];
//return \Event::fire('filter.settings_sections', [$sections]);
$sections = \Eventy::filter('settings.sections', $sections);
return $sections;
}
@@ -167,25 +275,83 @@ class SettingsController extends Controller
$request = request();
$request->settings = array_merge($request->settings, [
'email_branding' => !empty($request->settings['email_branding']) ? $request->settings['email_branding'] : 0,
'open_tracking' => !empty($request->settings['open_tracking']) ? $request->settings['open_tracking'] : 0,
'enrich_customer_data' => !empty($request->settings['enrich_customer_data']) ? $request->settings['enrich_customer_data'] : 0,
]);
$request = \Eventy::filter('settings.before_save', $request, $section, $settings);
$cc_required = false;
$settings_params = $this->getSectionParams($section, 'settings');
foreach ($settings as $i => $option_name) {
// By some reason isset() does not work for empty elements
// Do not save dummy passwords.
if (!empty($settings_params[$option_name])
&& !empty($settings_params[$option_name]['safe_password'])
&& $request->settings[$option_name]
&& preg_match("/^\*+$/", $request->settings[$option_name])
) {
continue;
}
// Option has to be saved to .env file.
if (!empty($settings_params[$option_name]) && !empty($settings_params[$option_name]['env'])) {
$env_value = $request->settings[$option_name] ?? '';
if (is_array($env_value)) {
$env_value = json_encode($env_value);
}
if (!empty($settings_params[$option_name]['encrypt'])) {
$env_value = encrypt($env_value);
}
if (!empty($settings_params[$option_name]['env_encode'])) {
$env_value = base64_encode($env_value);
}
\Helper::setEnvFileVar($settings_params[$option_name]['env'], $env_value);
config($option_name, $env_value);
$cc_required = true;
continue;
}
// By some reason isset() does not work for empty elements.
if (array_key_exists($option_name, $request->settings)) {
$option_value = $request->settings[$option_name];
if (!empty($settings_params[$option_name]['encrypt'])) {
$option_value = encrypt($option_value);
}
Option::set($option_name, $option_value);
} else {
Option::remove($option_name);
// If option does not exist, default will be used,
// so we can not just remove bool settings.
if (isset($settings_params[$option_name]['default'])) {
$default = $settings_params[$option_name]['default'];
} else {
$default = \Option::getDefault($option_name, null);
}
if ($default === true) {
Option::set($option_name, false);
} elseif (is_array(\Option::getDefault($option_name, -1))) {
Option::set($option_name, []);
} else {
Option::remove($option_name);
}
}
}
\Session::flash('flash_success_floating', __('Settings updated'));
// Clear cache if some options have been saved to .env file.
if ($cc_required) {
\Helper::clearCache(['--doNotGenerateVars' => true]);
}
return redirect()->route('settings', ['section' => $section]);
// \Helper::clearCache prevents \Session::flash() from displaying.
$request->session()->flash('flash_success_floating', __('Settings updated'));
$response = redirect()->route('settings', ['section' => $section]);
$response = \Eventy::filter('settings.after_save', $response, $request, $section, $settings);
return $response;
}
/**
@@ -219,7 +385,7 @@ class SettingsController extends Controller
}
if (!$test_result && !$response['msg']) {
$response['msg'] = __('Error occurend sending email. Please check your mail server logs for more details.');
$response['msg'] = __('Error occurred sending email. Please check your mail server logs for more details.');
}
}

View File

@@ -10,6 +10,8 @@ use Symfony\Component\Console\Output\BufferedOutput;
class SystemController extends Controller
{
public static $latest_version_error = '';
/**
* Create a new controller instance.
*
@@ -17,7 +19,9 @@ class SystemController extends Controller
*/
public function __construct()
{
$this->middleware('auth');
$this->middleware('auth', ['except' => [
'cron'
]]);
}
/**
@@ -41,13 +45,65 @@ class SystemController extends Controller
}
}
// Functions.
$functions = [
'shell_exec (PHP)' => function_exists('shell_exec'),
'proc_open (PHP)' => function_exists('proc_open'),
'ps (shell)' => function_exists('shell_exec') ? shell_exec('ps') : false,
];
// Permissions.
$permissions = [];
foreach (config('installer.permissions') as $perm_path => $perm_value) {
$path = base_path($perm_path);
$value = '';
if (file_exists($path)) {
$value = substr(sprintf('%o', fileperms($path)), -4);
}
$permissions[$perm_path] = [
'status' => \Helper::isFolderWritable($path),
'value' => $value,
];
}
// 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 = '';
}
}
// Check if public symlink exists, if not, try to create.
$public_symlink_exists = true;
$public_path = public_path('storage');
$public_test = $public_path.DIRECTORY_SEPARATOR.'.gitignore';
if (!file_exists($public_test) || !file_get_contents($public_test)) {
\File::delete($public_path);
\Artisan::call('storage:link');
if (!file_exists($public_test) || !file_get_contents($public_test)) {
$public_symlink_exists = false;
}
}
// Check if .env is writable.
$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();
$failed_queues = $failed_jobs->pluck('queue')->unique();
// Commands
$commands_list = ['freescout:fetch-emails', 'queue:work'];
foreach ($commands_list as $command_name) {
$commands_list = [
'freescout:fetch-emails' => 'freescout:fetch-emails',
\Helper::getWorkerIdentifier() => 'queue:work'
];
foreach ($commands_list as $command_identifier => $command_name) {
$status_texts = [];
// Check if command is running now
@@ -55,7 +111,7 @@ class SystemController extends Controller
$running_commands = 0;
try {
$processes = preg_split("/[\r\n]/", shell_exec("ps aux | grep '{$command_name}'"));
$processes = preg_split("/[\r\n]/", shell_exec("ps aux | grep '{$command_identifier}'"));
$pids = [];
foreach ($processes as $process) {
preg_match("/^[\S]+\s+([\d]+)\s+/", $process, $m);
@@ -77,7 +133,7 @@ class SystemController extends Controller
} elseif ($running_commands > 1) {
// queue:work command is stopped by settings a cache key
if ($command_name == 'queue:work') {
\Cache::forever('illuminate:queue:restart', Carbon::now()->getTimestamp());
\Helper::queueWorkRestart();
$commands[] = [
'name' => $command_name,
'status' => 'error',
@@ -120,9 +176,10 @@ 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 && function_exists('shell_exec')) {
$status_texts[] = __('Cleared cache to force command to start.');
\Artisan::call('cache:clear');
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>']);
// This sometimes makes Status page open as non logged in user.
//\Artisan::call('freescout:clear-cache', ['--doNotGenerateVars' => true]);
}
$commands[] = [
@@ -132,24 +189,67 @@ class SystemController extends Controller
];
}
// Check new version
// Check new version if enabled
$new_version_available = false;
$latest_version = \Cache::remember('latest_version', 15, function() {
return \Updater::getVersionAvailable();
});
if ($latest_version && $latest_version != \Config::get('app.version')) {
$new_version_available = true;
if (!\Config::get('app.disable_updating')) {
$latest_version = \Cache::remember('latest_version', 15, function () {
try {
return \Updater::getVersionAvailable();
} catch (\Exception $e) {
SystemController::$latest_version_error = $e->getMessage();
return '';
}
});
if ($latest_version && version_compare($latest_version, \Config::get('app.version'), '>')) {
$new_version_available = true;
}
} else {
$latest_version = \Config::get('app.version');
}
return view('system/status', [
'commands' => $commands,
'queued_jobs' => $queued_jobs,
'failed_jobs' => $failed_jobs,
'php_extensions' => $php_extensions,
'commands' => $commands,
'queued_jobs' => $queued_jobs,
'failed_jobs' => $failed_jobs,
'failed_queues' => $failed_queues,
'php_extensions' => $php_extensions,
'functions' => $functions,
'permissions' => $permissions,
'new_version_available' => $new_version_available,
'latest_version' => $latest_version,
'latest_version_error' => SystemController::$latest_version_error,
'public_symlink_exists' => $public_symlink_exists,
'env_is_writable' => $env_is_writable,
'non_writable_cache_file' => $non_writable_cache_file,
]);
}
public function action(Request $request)
{
switch ($request->action) {
case 'cancel_job':
\App\Job::where('id', $request->job_id)->delete();
\Session::flash('flash_success_floating', __('Done'));
break;
case 'delete_failed_jobs':
\App\FailedJob::where('queue', $request->failed_queue)->delete();
\Session::flash('flash_success_floating', __('Failed jobs deleted'));
break;
case 'retry_failed_jobs':
$jobs = \App\FailedJob::where('queue', $request->failed_queue)->get();
foreach ($jobs as $job) {
\Artisan::call('queue:retry', ['id' => $job->id]);
}
\Session::flash('flash_success_floating', __('Failed jobs restarted'));
break;
}
return redirect()->route('system');
}
/**
* System tools.
*/
@@ -161,18 +261,20 @@ class SystemController extends Controller
}
return view('system/tools', [
'output' => $output
'output' => $output,
]);
}
/**
* Execute tools action.
* @param Request $request [description]
* @return [type] [description]
*
* @param Request $request [description]
*
* @return [type] [description]
*/
public function toolsExecute(Request $request)
{
$outputLog = new BufferedOutput;
$outputLog = new BufferedOutput();
switch ($request->action) {
case 'clear_cache':
@@ -180,7 +282,18 @@ class SystemController extends Controller
break;
case 'fetch_emails':
\Artisan::call('freescout:fetch-emails', [], $outputLog);
$params = [];
$params['--days'] = (int)$request->days;
$params['--unseen'] = (int)$request->unseen;
\Artisan::call('freescout:fetch-emails', $params, $outputLog);
break;
case 'migrate_db':
\Artisan::call('migrate', ['--force' => true], $outputLog);
break;
case 'logout_users':
\Artisan::call('freescout:logout-users', [], $outputLog);
break;
}
@@ -192,7 +305,7 @@ class SystemController extends Controller
\Cache::forever('tools_execute_output', $output);
}
return redirect()->route('system.tools');
return redirect()->route('system.tools')->withInput($request->input());
}
/**
@@ -210,26 +323,34 @@ class SystemController extends Controller
case 'update':
try {
$status = \Updater::update();
// Artisan::output()
} catch (\Exception $e) {
$response['msg'] = __('Error occured').': '.$e->getMessage();
$response['msg'] = __('Error occured. Please try again or try another :%a_start%update method:%a_end%', ['%a_start%' => '<a href="'.config('app.freescout_url').'/docs/update/" target="_blank">', '%a_end%' => '</a>']);
$response['msg'] .= '<br/><br/>'.$e->getMessage();
\Helper::logException($e);
}
if (!$response['msg'] && $status) {
// Adding session flash is useless as cache is cleated
// Adding session flash is useless as cache is cleared
$response['msg_success'] = __('Application successfully updated');
$response['status'] = 'success';
}
break;
case 'check_updates':
try {
$response['new_version_available'] = \Updater::isNewVersionAvailable(config('app.version'));
$response['status'] = 'success';
} catch (\Exception $e) {
$response['msg'] = __('Error occured').': '.$e->getMessage();
}
if (!$response['msg'] && !$response['new_version_available']) {
// Adding session flash is useless as cache is cleated
if (!\Config::get('app.disable_updating')) {
try {
$response['new_version_available'] = \Updater::isNewVersionAvailable(config('app.version'));
$response['status'] = 'success';
} catch (\Exception $e) {
$response['msg'] = __('Error occured').': '.$e->getMessage();
}
if (!$response['msg'] && !$response['new_version_available']) {
// Adding session flash is useless as cache is cleated
$response['msg_success'] = __('You have the latest version installed');
}
} else {
$response['msg_success'] = __('You have the latest version installed');
}
break;
@@ -245,4 +366,19 @@ class SystemController extends Controller
return \Response::json($response);
}
/**
* Web Cron.
*/
public function cron(Request $request)
{
if (empty($request->hash) || $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');
}
}

View File

@@ -2,24 +2,44 @@
namespace App\Http\Controllers;
use \Barryvdh\TranslationManager\Models\Translation;
use Barryvdh\TranslationManager\Controller as BaseController;
class TranslateController extends BaseController
{
/**
* Send translations to FreeScout team.
*
* @return [type] [description]
*/
public function postSend()
{
$result = false;
// Count changed translations.
$changed_data = Translation::select(['locale', 'group', \DB::raw('count(*) as changed')])
->where('status', Translation::STATUS_CHANGED)
->groupBy(['locale', 'group'])
->get()
->toArray();
$this->manager->exportTranslations('*', false);
// Archive langs folder
$archive_path = \Helper::createZipArchive(base_path().DIRECTORY_SEPARATOR.'resources/lang', 'lang.zip', 'lang');
try {
$archive_path = \Helper::createZipArchive(base_path().DIRECTORY_SEPARATOR.'resources/lang', 'lang.zip', 'lang');
} catch (\Exception $e) {
return [
'status' => 'error',
'error_msg' => $e->getMessage(),
];
}
if ($archive_path) {
$attachments[] = $archive_path;
// Send archive to developers
$result = \MailHelper::sendEmailToDevs('Translations', '', $attachments, auth()->user());
$result = \MailHelper::sendEmailToDevs('Translations', json_encode($changed_data), $attachments, auth()->user());
}
if ($result) {
@@ -28,4 +48,47 @@ class TranslateController extends BaseController
abort(500);
}
}
/**
* Remove all translations which has not been published yet.
*
* @return [type] [description]
*/
public function postRemoveUnpublished()
{
\Barryvdh\TranslationManager\Models\Translation::truncate();
return ['status' => 'ok'];
}
/**
* Download as ZIP.
*
* @return [type] [description]
*/
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');
$public_path = storage_path('app/public/'.$file_name);
\File::copy($archive_path, $public_path);
$headers = [
'Content-Type: application/zip',
];
return \Response::download($public_path, $file_name, $headers);
}
/**
* List of strings to translate.
*/
public function stringsToTranslate()
{
__(':field is required');
__('The following modules have to be installed and activated: :modules');
}
}

View File

@@ -6,6 +6,7 @@ use App\Events\UserDeleted;
use App\Folder;
use App\Mailbox;
use App\Subscription;
use App\Thread;
use App\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
@@ -32,7 +33,8 @@ class UsersController extends Controller
{
$this->authorize('create', 'App\User');
$users = User::all();
$users = User::nonDeleted()->get();
$users = User::sortUsers($users);
return view('users/users', ['users' => $users]);
}
@@ -55,21 +57,30 @@ class UsersController extends Controller
*/
public function createSave(Request $request)
{
$invalid = false;
$this->authorize('create', 'App\User');
$auth_user = auth()->user();
$rules = [
'role' => 'integer',
'first_name' => 'required|string|max:20',
'last_name' => 'required|string|max:30',
'email' => 'required|string|email|max:100|unique:users',
'role' => ['required', Rule::in(array_keys(User::$roles))],
//'role' => ['required', Rule::in(array_keys(User::$roles))],
];
if ($auth_user->isAdmin()) {
$rules['role'] = ['required', Rule::in(array_keys(User::$roles))];
}
if (empty($request->send_invite)) {
$rules['password'] = 'required|string|max:255';
}
$validator = Validator::make($request->all(), $rules);
if ($validator->fails()) {
if (User::mailboxEmailExists($request->email)) {
$invalid = true;
$validator->errors()->add('email', __('There is a mailbox with such email. Users and mailboxes can not have the same email addresses.'));
}
if ($invalid || $validator->fails()) {
return redirect()->route('users.create')
->withErrors($validator)
->withInput();
@@ -77,6 +88,9 @@ class UsersController extends Controller
$user = new User();
$user->fill($request->all());
if (!$auth_user->can('changeRole', $user)) {
$user->role = User::ROLE_USER;
}
if (empty($request->send_invite)) {
// Set password from request
$user->password = Hash::make($request->password);
@@ -110,6 +124,9 @@ class UsersController extends Controller
public function profile($id)
{
$user = User::findOrFail($id);
if ($user->isDeleted()) {
abort(404);
}
$this->authorize('update', $user);
@@ -121,7 +138,7 @@ class UsersController extends Controller
public function getUsersForSidebar($except_id)
{
if (auth()->user()->isAdmin()) {
return User::all()->except($except_id);
return User::sortUsers(User::nonDeleted()->get());/*->except($except_id)*/;
} else {
return [];
}
@@ -136,6 +153,8 @@ class UsersController extends Controller
*/
public function profileSave($id, Request $request)
{
$invalid = false;
$user = User::findOrFail($id);
$this->authorize('update', $user);
@@ -144,7 +163,7 @@ class UsersController extends Controller
'first_name' => 'required|string|max:20',
'last_name' => 'required|string|max:30',
'email' => 'required|string|email|max:100|unique:users,email,'.$id,
'emails' => 'max:100',
//'emails' => 'max:100',
'job_title' => 'max:100',
'phone' => 'max:60',
'timezone' => 'required|string|max:255',
@@ -164,6 +183,7 @@ class UsersController extends Controller
if ($path_url) {
$user->photo_url = $path_url;
} else {
$invalid = true;
$validator->errors()->add('photo_url', __('Error occured processing the image. Make sure that PHP GD extension is enabled.'));
}
}
@@ -172,17 +192,28 @@ class UsersController extends Controller
if ($user->isAdmin() && isset($request->role) && $request->role != User::ROLE_ADMIN) {
$admins_count = User::where('role', User::ROLE_ADMIN)->count();
if ($admins_count < 2) {
$invalid = true;
$validator->errors()->add('role', __('Role of the only one administrator can not be changed.'));
}
}
});
if ($validator->fails()) {
if (User::mailboxEmailExists($request->email)) {
$invalid = true;
$validator->errors()->add('email', __('There is a mailbox with such email. Users and mailboxes can not have the same email addresses.'));
}
if ($invalid || $validator->fails()) {
return redirect()->route('users.profile', ['id' => $id])
->withErrors($validator)
->withInput();
}
// Save language into session.
if (auth()->user()->id == $id && $request->locale) {
session()->put('user_locale', $request->locale);
}
$request_data = $request->all();
if (isset($request_data['photo_url'])) {
@@ -191,6 +222,13 @@ class UsersController extends Controller
if (!auth()->user()->can('changeRole', $user)) {
unset($request_data['role']);
}
if ($user->status != User::STATUS_DELETED) {
if (!empty($request_data['disabled'])) {
$request_data['status'] = User::STATUS_DISABLED;
} else {
$request_data['status'] = User::STATUS_ACTIVE;
}
}
$user->fill($request_data);
if (empty($request->input('enable_kb_shortcuts'))) {
@@ -221,8 +259,8 @@ class UsersController extends Controller
$users = $this->getUsersForSidebar($id);
return view('users/permissions', [
'user' => $user,
'mailboxes' => $mailboxes,
'user' => $user,
'mailboxes' => $mailboxes,
'user_mailboxes' => $user->mailboxes,
'users' => $users,
]);
@@ -240,12 +278,27 @@ class UsersController extends Controller
if (!$user->isAdmin()) {
abort(403);
}
$user = User::findOrFail($id);
$user->mailboxes()->sync($request->mailboxes);
$user->syncPersonalFolders($request->mailboxes);
// Save permissions.
$user_permissions = $request->user_permissions ?? [];
$permissions = [];
foreach (User::getUserPermissionsList() as $permission_id) {
$new_has_permission = in_array($permission_id, $user_permissions);
if ($user->hasPermission($permission_id, false) != $new_has_permission) {
$permissions[$permission_id] = (int)(bool)$new_has_permission;
$save_user = true;
}
}
$user->permissions = $permissions;
$user->save();
\Session::flash('flash_success_floating', __('Permissions saved successfully'));
return redirect()->route('users.permissions', ['id' => $id]);
@@ -273,6 +326,7 @@ class UsersController extends Controller
'subscriptions' => $subscriptions,
'person' => $person,
'users' => $users,
'mobile_available' => \Eventy::filter('notifications.mobile_available', false),
]);
}
@@ -311,10 +365,10 @@ class UsersController extends Controller
// Both send and resend
case 'send_invite':
if (!$auth_user->isAdmin()) {
$response['msg'] = __('Not enough permissions');
$response['msg'] = __('Not enough permissions');
}
if (empty($request->user_id)) {
$response['msg'] = __('Incorrect user');
$response['msg'] = __('Incorrect user');
}
if (!$response['msg']) {
$user = User::find($request->user_id);
@@ -340,10 +394,10 @@ class UsersController extends Controller
// Reset password
case 'reset_password':
if (!auth()->user()->isAdmin()) {
$response['msg'] = __('Not enough permissions');
$response['msg'] = __('Not enough permissions');
}
if (empty($request->user_id)) {
$response['msg'] = __('Incorrect user');
$response['msg'] = __('Incorrect user');
}
if (!$response['msg']) {
$user = User::find($request->user_id);
@@ -368,7 +422,7 @@ class UsersController extends Controller
// Load website notifications
case 'web_notifications':
if (!$auth_user) {
$response['msg'] = __('You are not logged in');
$response['msg'] = __('You are not logged in');
}
if (!$response['msg']) {
$web_notifications_info = $auth_user->getWebsiteNotificationsInfo(false);
@@ -376,8 +430,8 @@ class UsersController extends Controller
'web_notifications_info_data' => $web_notifications_info['data'],
])->render();
$response['has_more_pages'] = (int)$web_notifications_info['notifications']->hasMorePages();
$response['has_more_pages'] = (int) $web_notifications_info['notifications']->hasMorePages();
$response['status'] = 'success';
}
break;
@@ -385,7 +439,7 @@ class UsersController extends Controller
// Mark all user website notifications as read
case 'mark_notifications_as_read':
if (!$auth_user) {
$response['msg'] = __('You are not logged in');
$response['msg'] = __('You are not logged in');
}
if (!$response['msg']) {
$auth_user->unreadNotifications()->update(['read_at' => now()]);
@@ -431,53 +485,85 @@ class UsersController extends Controller
}
if (!$response['msg']) {
event(new UserDeleted($user, $auth_user));
// We have to process conversations one by one to set move them to Unassigned folder,
// 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->conversations->each(function ($conversation) {
$user->conversations->each(function ($conversation) use ($auth_user, $request) {
// We don't fire ConversationUserChanged event to avoid sending notifications to users
$conversation->user_id = null;
$folder_id = null;
if (!empty($mailbox_unassigned_folders[$conversation->mailbox_id])) {
$folder_id = $mailbox_unassigned_folders[$conversation->mailbox_id];
if (!empty($request->assign_user) && !empty($request->assign_user[$conversation->mailbox_id]) && (int) $request->assign_user[$conversation->mailbox_id] != -1) {
// Set assignee.
$conversation->user_id = $request->assign_user[$conversation->mailbox_id];
// In this case conversation stays assigned, just assignee changes.
} else {
$folder = $conversation->mailbox->folders()
->where('type', Folder::TYPE_UNASSIGNED)
->first();
// Set assignee.
$conversation->user_id = null;
if ($folder) {
$folder_id = $folder->id;
$mailbox_unassigned_folders[$conversation->mailbox_id] = $folder_id;
// Change conversation folder to ANASSIGNED.
$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;
}
}
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();
});
}
//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->delete();
\Session::flash('flash_success_floating', __('User deleted'));
$user->status = \App\User::STATUS_DELETED;
// Update email.
$email_suffix = '_deleted'.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());
$response['status'] = 'success';
}
@@ -533,7 +619,7 @@ class UsersController extends Controller
// Check current password
if (!Hash::check($request->password_current, $user->password)) {
$validator->errors()->add('password_current', __('This password is incorrect.'));
} else if (Hash::check($request->password, $user->password)) {
} elseif (Hash::check($request->password, $user->password)) {
// Check new password
$validator->errors()->add('password', __('The new password is the same as the old password.'));
}

View File

@@ -33,17 +33,21 @@ class Kernel extends HttpKernel
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
\App\Http\Middleware\TokenAuth::class,
// \Illuminate\Session\Middleware\AuthenticateSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
\App\Http\Middleware\HttpsRedirect::class,
\App\Http\Middleware\Localize::class,
\App\Http\Middleware\LogoutIfDeleted::class,
\App\Http\Middleware\CustomHandle::class,
],
'api' => [
'throttle:60,1',
'bindings',
],
// 'api' => [
// 'throttle:60,1',
// 'bindings',
// ],
];
/**

View File

@@ -0,0 +1,24 @@
<?php
namespace App\Http\Middleware;
use Closure;
class CustomHandle
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
*
* @return mixed
*/
public function handle($request, Closure $next)
{
// Hook.
\Eventy::action('middleware.web.custom_handle', $request);
return \Eventy::filter('middleware.web.custom_handle.response', $next($request), $request, $next);
}
}

View File

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

View File

@@ -9,14 +9,32 @@ namespace App\Http\Middleware;
use Closure;
use Illuminate\Support\Facades\App;
use Illuminate\Http\Request;
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',
];
public function handle($request, Closure $next)
{
if (\Config::get('app.force_https') == 'true') {
$request->setTrustedProxies( [ $request->getClientIp() ] );
if (!$request->secure() /*&& App::environment() === 'production'*/) {
$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"}'
) {
return redirect()->secure($request->getRequestUri());
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace App\Http\Middleware;
use Closure;
class Localize
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
*
* @return mixed
*/
public function handle($request, Closure $next)
{
// Interface language is set automatically, as locale is stored in .env file.
// Set user language if user logged in.
$user_locale = \Eventy::filter('locale', session('user_locale'));
if ($user_locale) {
\Helper::setLocale($user_locale);
}
return $next($request);
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Redirect;
class LogoutIfDeleted
{
/**
* 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)
{
$user = Auth::user();
if ($user && ($user->isDeleted() || $user->isDisabled())) {
Auth::logout();
return Redirect::route('login');
}
return $next($request);
}
}

View File

@@ -12,9 +12,9 @@ class ResponseHeaders
// 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');
}
$response->header('Pragma', 'no-cache');
$response->header('Cache-Control', 'no-cache, max-age=0, must-revalidate, no-store');
}
return $response;
}

View File

@@ -0,0 +1,33 @@
<?php
namespace App\Http\Middleware;
use App\User;
use Closure;
class TokenAuth
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
*
* @return mixed
*/
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]');
}
if (!empty($user)) {
\Auth::login($user);
}
}
return $next($request);
}
}

View File

@@ -6,6 +6,8 @@ use Illuminate\Database\Eloquent\Model;
class Job extends Model
{
const UPDATED_AT = null;
/**
* Automatically converted into Carbon dates.
*/

View File

@@ -2,11 +2,12 @@
/**
* Send alert to super admin.
*/
namespace App\Jobs;
use App\Mail\Alert;
use App\SendLog;
use App\User;
use App\Mail\Alert;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
@@ -41,7 +42,7 @@ class SendAlert implements ShouldQueue
{
// Configure mail driver according to Mailbox settings
\MailHelper::setSystemMailDriver();
$recipients = [];
$super_admin = User::getSuperAdmin();

View File

@@ -55,10 +55,15 @@ class SendAutoReply implements ShouldQueue
$headers['Message-ID'] = $message_id;
$customer_email = $this->conversation->customer_email;
if (!$customer_email) {
// When message is received via Chat, customer has no email adddress.
return;
}
$recipients = [$customer_email];
$failures = [];
$exception = null;
$failures = [];
$exception = null;
try {
Mail::to([['name' => $this->customer->getFullName(), 'email' => $customer_email]])
->send(new AutoReply($this->conversation, $this->mailbox, $this->customer, $headers));

View File

@@ -7,7 +7,6 @@ namespace App\Jobs;
use App\Mail\UserEmailReplyError;
use App\SendLog;
use App\Thread;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
@@ -48,6 +47,7 @@ class SendEmailReplyError implements ShouldQueue
\App\Misc\Mail::setMailDriver($this->mailbox);
$exception = null;
try {
Mail::to([['name' => '', 'email' => $this->from]])
->send(new UserEmailReplyError());

View File

@@ -16,6 +16,9 @@ class SendNotificationToUsers implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
// Max retries + 1
public $tries = 168; // One per hour
public $users;
public $conversation;
@@ -48,17 +51,30 @@ class SendNotificationToUsers implements ShouldQueue
// Threads has to be sorted here, if sorted before, they come here in wrong order
$this->threads = $this->threads->sortByDesc(function ($item, $key) {
return $item->created_at;
return $item->id;
});
$headers = [];
$last_thread = $this->threads->first();
if (!$last_thread) {
return;
}
// If thread is draft, it means it has been undone
if ($last_thread->isDraft()) {
return;
}
// Limit conversation history
if (config('app.email_user_history') == 'last') {
$this->threads = $this->threads->slice(0, 2);
}
if (config('app.email_user_history') == 'none') {
$this->threads = $this->threads->slice(0, 1);
}
// All notification for the same conversation has same dummy Message-ID
$prev_message_id = \App\Misc\Mail::MESSAGE_ID_PREFIX_NOTIFICATION_IN_REPLY.'-'.$this->conversation->id.'-'.md5($this->conversation->id).'@'.$mailbox->getEmailDomain();
$headers['In-Reply-To'] = '<'.$prev_message_id.'>';
@@ -68,22 +84,50 @@ class SendNotificationToUsers implements ShouldQueue
$global_exception = null;
foreach ($this->users as $user) {
// User cam ne deleted.
if (!isset($user->id)) {
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) {
// Maybe add indexes to the table.
$already_sent = SendLog::where('thread_id', $last_thread->id)
->where('mail_type', SendLog::MAIL_TYPE_USER_NOTIFICATION)
->where('user_id', $user->id)
->whereIn('status', SendLog::$sent_success)
->exists();
if ($already_sent) {
continue;
}
}
$message_id = \App\Misc\Mail::MESSAGE_ID_PREFIX_NOTIFICATION.'-'.$last_thread->id.'-'.$user->id.'-'.time().'@'.$mailbox->getEmailDomain();
$headers['Message-ID'] = $message_id;
// If this is notification on message from customer set customer as sender name
// If this is notification on message from customer, set customer as sender name
$from_name = '';
if ($last_thread->type == Thread::TYPE_CUSTOMER) {
$from_name = $last_thread->customer->getFullName();
$from_name = '';
if ($last_thread->customer) {
$from_name = $last_thread->customer->getFullName(true, true);
}
if ($from_name) {
$from_name = $from_name.' '.__('via').' '.\Config::get('app.name');
$from_name = $from_name.' '.__('via').' '.$mailbox->name;
}
}
if (!$from_name) {
$from_name = \Config::get('app.name');
$from_name = $mailbox->name;
}
$from = ['address' => $mailbox->email, 'name' => $from_name];
// Set user language
app()->setLocale($user->getLocale());
$headers['X-FreeScout-Mail-Type'] = 'user.notification';
$exception = null;
try {
@@ -122,7 +166,23 @@ class SendNotificationToUsers implements ShouldQueue
}
if ($global_exception) {
throw $global_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) {
if ($this->attempts() == 1) {
// Second attempt after 5 min.
$this->release(300);
} else {
// Others - after 1 hour.
$this->release(3600);
}
throw $global_exception;
} else {
$this->fail($global_exception);
return;
}
}
}

View File

@@ -11,11 +11,15 @@ use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Mail;
use Webklex\IMAP\Client;
class SendReplyToCustomer implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
// Number of retries + 1
public $tries = 168; // one per hour
public $conversation;
public $threads;
@@ -47,47 +51,131 @@ class SendReplyToCustomer implements ShouldQueue
*/
public function handle()
{
$send_previous_messages = false;
$is_forward = false;
// When forwarding conversation is undone, new conversation is deleted.
if (!$this->conversation) {
return;
}
$mailbox = $this->conversation->mailbox;
// Mailbox may be deleted.
if (!$mailbox) {
return;
}
// Add forwarded conversation replies.
if ($this->conversation->threads_count == 1 && count($this->threads) == 1) {
$forward_child_thread = $this->threads[0];
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;
});
$forward_parent_thread = Thread::find($forward_child_thread->getMeta('forward_parent_thread_id'));
if ($forward_parent_thread) {
// Remove threads created after forwarding.
foreach ($forwarded_replies as $i => $thread) {
if ($thread->created_at > $forward_parent_thread->created_at) {
$forwarded_replies->forget($i);
}
}
$this->threads = $this->threads->merge($forwarded_replies);
$is_forward = true;
}
}
}
// Threads has to be sorted here, if sorted before, they come here in wrong order
$this->threads = $this->threads->sortByDesc(function ($item, $key) {
return $item->created_at;
return $item->id;
});
$new = false;
$headers = [];
$this->last_thread = $this->threads->first();
$prev_thread = null;
if ($this->last_thread === null) {
return;
}
$last_customer_thread = null;
// If thread is draft, it means it has been undone
if ($this->last_thread->isDraft()) {
return;
}
// Configure mail driver according to Mailbox settings
\App\Misc\Mail::setMailDriver($mailbox, $this->last_thread->created_by_user);
if (count($this->threads) == 1) {
$new = true;
}
$i = 0;
foreach ($this->threads as $thread) {
if ($i == 1) {
$prev_thread = $thread;
break;
if (!$new) {
$i = 0;
foreach ($this->threads as $thread) {
if ($i > 0 && $thread->type == Thread::TYPE_CUSTOMER) {
$last_customer_thread = $thread;
break;
}
$i++;
}
$i++;
}
// Get penultimate email Message-Id if reply
if (!$new && !empty($prev_thread) && $prev_thread->message_id) {
$headers['In-Reply-To'] = '<'.$prev_thread->message_id.'>';
$headers['References'] = '<'.$prev_thread->message_id.'>';
// Conversation history.
$email_conv_history = config('app.email_conv_history');
$meta_conv_history = $this->last_thread->getMeta(Thread::META_CONVERSATION_HISTORY);
if (!empty($meta_conv_history)) {
$email_conv_history = $meta_conv_history;
}
if ($is_forward && $email_conv_history == 'global') {
$email_conv_history = 'full';
}
if ($is_forward && $email_conv_history == 'none') {
$email_conv_history = 'full';
}
if ($email_conv_history == 'full') {
$send_previous_messages = true;
}
if ($email_conv_history == 'last') {
$send_previous_messages = true;
$this->threads = $this->threads->slice(0, 2);
}
if ($email_conv_history == 'none') {
$send_previous_messages = false;
}
if (!$is_forward) {
$send_previous_messages = \Eventy::filter('jobs.send_reply_to_customer.send_previous_messages', $send_previous_messages, $this->last_thread, $this->threads, $this->conversation, $this->customer);
}
// Remove previous messages.
if (!$send_previous_messages) {
$this->threads = $this->threads->slice(0, 1);
}
// Configure mail driver according to Mailbox settings
\App\Misc\Mail::setMailDriver($mailbox, $this->last_thread->created_by_user);
// 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.'>';
}
$this->message_id = \App\Misc\Mail::MESSAGE_ID_PREFIX_REPLY_TO_CUSTOMER.'-'.$this->last_thread->id.'-'.md5($this->last_thread->id).'@'.$mailbox->getEmailDomain();
$headers['Message-ID'] = $this->message_id;
$this->customer_email = $this->conversation->customer_email;
$to_array = $mailbox->removeMailboxEmailsFromList($this->last_thread->getToArray());
$cc_array = $mailbox->removeMailboxEmailsFromList($this->last_thread->getCcArray());
$bcc_array = $mailbox->removeMailboxEmailsFromList($this->last_thread->getBccArray());
@@ -95,35 +183,125 @@ class SendReplyToCustomer implements ShouldQueue
$cc_array = \App\Misc\Mail::removeEmailFromArray($cc_array, $this->customer_email);
$bcc_array = \App\Misc\Mail::removeEmailFromArray($bcc_array, $this->customer_email);
// Auto Bcc.
if ($mailbox->auto_bcc) {
$auto_bcc = \MailHelper::sanitizeEmails($mailbox->auto_bcc);
if ($auto_bcc) {
$bcc_array = array_merge($bcc_array, $auto_bcc);
}
}
// Remove from BCC emails which are present in CC
foreach ($cc_array as $cc_email) {
$bcc_array = \App\Misc\Mail::removeEmailFromArray($bcc_array, $cc_email);
}
$this->recipients = array_merge([$this->customer_email], $cc_array, $bcc_array);
$this->recipients = array_merge($to_array, $cc_array, $bcc_array);
$to = [];
if (count($to_array) > 1) {
$to = $to_array;
} else {
$to = [['name' => $this->customer->getFullName(), 'email' => $this->customer_email]];
}
// If sending fails, all recipiens fail.
// if ($this->attempts() > 1) {
// $cc_array = [];
// $bcc_array = [];
// }
$headers['X-FreeScout-Mail-Type'] = 'customer.message';
$reply_mail = new ReplyToCustomer($this->conversation, $this->threads, $headers, $mailbox);
try {
Mail::to([['name' => $this->customer->getFullName(), 'email' => $this->customer_email]])
Mail::to($to)
->cc($cc_array)
->bcc($bcc_array)
->send(new ReplyToCustomer($this->conversation, $this->threads, $headers, $mailbox));
->send($reply_mail);
} catch (\Exception $e) {
// We come here in case SMTP server unavailable for example
activity()
->causedBy($this->customer)
->withProperties([
'error' => $e->getMessage().'; File: '.$e->getFile().' ('.$e->getLine().')',
])
->useLog(\App\ActivityLog::NAME_EMAILS_SENDING)
->log(\App\ActivityLog::DESCRIPTION_EMAILS_SENDING_ERROR_TO_CUSTOMER);
if ($this->attempts() == 1) {
activity()
->causedBy($this->customer)
->withProperties([
'error' => $e->getMessage().'; File: '.$e->getFile().' ('.$e->getLine().')',
])
->useLog(\App\ActivityLog::NAME_EMAILS_SENDING)
->log(\App\ActivityLog::DESCRIPTION_EMAILS_SENDING_ERROR_TO_CUSTOMER);
}
// Failures will be saved to send log when retry attempts will finish
// Mail::failures() is empty in case of connection error.
$this->failures = $this->recipients;
// Save to send log
$this->saveToSendLog($e->getMessage());
// Save to send log (only first attempt).
if ($this->attempts() == 1) {
$this->saveToSendLog($e->getMessage());
}
throw $e;
$error_message = $e->getMessage();
// 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)) {
if ($this->attempts() == 1) {
// Second attempt after 5 min.
$this->release(300);
} else {
// Others - after 1 hour.
$this->release(3600);
}
throw $e;
} else {
$this->last_thread->send_status = SendLog::STATUS_SEND_ERROR;
$this->last_thread->updateSendStatusData(['msg' => $error_message]);
$this->last_thread->save();
// This executes $this->failed().
$this->fail($e);
return;
}
}
$imap_sent_folder = $mailbox->imap_sent_folder;
if ($imap_sent_folder) {
try {
$client = \MailHelper::getMailboxClient($mailbox);
$client->connect();
$envelope['from'] = $mailbox->getMailFrom()['address'];
$envelope['to'] = $this->customer_email;
$envelope['subject'] = 'Re: ' . $this->conversation->subject;
// 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.'>'
];
}
$part1['type'] = TYPETEXT;
$part1['subtype'] = 'html';
$part1['contents.data'] = $reply_mail->render();
try {
$folder = $client->getFolder($imap_sent_folder);
$folder->appendMessage(imap_mail_compose($envelope, [$part1]), '\Seen', now()->format('d-M-Y H:i:s O'));
} 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.' - ');
//$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.' - ');
}
}
// In message_id we are storing Message-ID of the incoming email which created the thread

View File

@@ -0,0 +1,44 @@
<?php
/**
* Used to tirgger Eventy actions with delay.
*/
namespace App\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class TriggerAction implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $action;
public $params;
/**
* Create a new job instance.
*
* @return void
*/
public function __construct($action, $params)
{
$this->action = $action;
$this->params = $params;
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
$args = $this->params;
array_unshift($args, $this->action);
call_user_func_array("\Eventy::action", $args);
}
}

View File

@@ -26,7 +26,7 @@ class LogUserDeletion
public function handle(UserDeleted $event)
{
activity()
->causedBy($event->user)
->causedBy($event->by_user)
->withProperties(['deleted_user' => $event->deleted_user->getFullName().' ['.$event->deleted_user->id.']'])
->useLog(\App\ActivityLog::NAME_USER)
->log(\App\ActivityLog::DESCRIPTION_USER_DELETED);

View File

@@ -0,0 +1,25 @@
<?php
namespace App\Listeners;
class ProcessSwiftMessage
{
/**
* Create the event listener.
*
* @return void
*/
public function __construct()
{
//
}
/**
* Handle the event.
*/
public function handle($event)
{
return \Eventy::filter('mail.process_swift_message', true, $event->message);
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace App\Listeners;
use App\Conversation;
class RefreshConversations
{
/**
* Create the event listener.
*
* @return void
*/
public function __construct()
{
//
}
/**
* Handle the event.
*
* @param $event
*
* @return void
*/
public function handle($event)
{
Conversation::refreshConversations($event->conversation, $event->thread ?? $event->last_thread);
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace App\Listeners;
use Illuminate\Auth\Events\Login;
class RememberUserLocale
{
/**
* Create the event listener.
*
* @return void
*/
public function __construct()
{
//
}
/**
* Handle the event.
*
* @param Login $event
*
* @return void
*/
public function handle(Login $event)
{
// Save user locale to session to show user app in his chosen language.
session()->put('user_locale', $event->user->getLocale());
}
}

View File

@@ -2,10 +2,14 @@
namespace App\Listeners;
use App\Conversation;
use App\Mailbox;
use App\SendLog;
class SendAutoReply
{
const CHECK_PERIOD = 180; // min
/**
* Create the event listener.
*
@@ -23,7 +27,63 @@ class SendAutoReply
{
$conversation = $event->conversation;
if (/*!$conversation->imported &&*/ $conversation->mailbox->auto_reply_enabled) {
if (!$conversation->imported && $conversation->mailbox->auto_reply_enabled) {
$thread = $conversation->threads()->first();
// Do not send auto reply to auto responders.
if ($thread->isAutoResponder()) {
return;
}
// Do not send auto replies to bounces.
if ($thread->isBounce()) {
return;
}
// Do not send auto replies to spam messages.
if ($conversation->status == Conversation::STATUS_SPAM) {
return;
}
if (!\Eventy::filter('autoreply.should_send', true, $conversation)) {
return;
}
// We can not send auto reply to incoming bounce messages, as it will lead to the infinite loop:
// application will be sending auto replies and mail server will be sending bounce messages to auto replies.
// Bounce detection can not be 100% reliable.
// So to prevent infinite loop, we are checking number of auto replies sent to the customer recently.
$created_at = \Illuminate\Support\Carbon::now()->subMinutes(self::CHECK_PERIOD);
$auto_replies_sent = SendLog::where('customer_id', $conversation->customer_id)
->where('mail_type', SendLog::MAIL_TYPE_AUTO_REPLY)
->where('created_at', '>', $created_at)
->count();
if ($auto_replies_sent >= 10) {
return;
}
if ($auto_replies_sent >= 2) {
// Find conversations from this customer
$prev_conversations = Conversation::select('subject', 'id')
->where('customer_id', $conversation->customer_id)
->where('created_at', '>', $created_at)
->get();
foreach ($prev_conversations as $prev_conv) {
if ($prev_conv->subject == $conversation->subject && $prev_conv->id != $conversation->id) {
return;
}
}
}
// Do not send autoreplies to own mailboxes.
if ($conversation->customer_email) {
$is_internal_email = Mailbox::where('email', $conversation->customer_email)->exists();
if ($is_internal_email) {
return;
}
}
// 24h limit has been disabled: https://github.com/freescout-helpdesk/freescout/pull/95
// Send auto reply once in 24h
@@ -37,7 +97,7 @@ class SendAutoReply
return;
}*/
\App\Jobs\SendAutoReply::dispatch($conversation, $conversation->threads()->first(), $conversation->mailbox, $conversation->customer)
\App\Jobs\SendAutoReply::dispatch($conversation, $thread, $conversation->mailbox, $conversation->customer)
->onQueue('emails');
}
}

View File

@@ -1,7 +1,10 @@
<?php
/**
* Send notifications to users by email and in browser.
*/
namespace App\Listeners;
use App\Conversation;
use App\Subscription;
class SendNotificationToUsers
@@ -32,14 +35,21 @@ class SendNotificationToUsers
break;
case 'App\Events\UserAddedNote':
$caused_by_user_id = $event->thread->created_by_user_id;
$event_type = Subscription::EVENT_TYPE_USER_ADDED_NOTE;
// When conversation is forwarded only notification
// about child forward conversation is sent.
if (!$event->thread->isForward()) {
$event_type = Subscription::EVENT_TYPE_USER_ADDED_NOTE;
}
break;
case 'App\Events\UserCreatedConversation':
$caused_by_user_id = $event->conversation->created_by_user_id;
$event_type = Subscription::EVENT_TYPE_NEW;
break;
case 'App\Events\CustomerCreatedConversation':
$event_type = Subscription::EVENT_TYPE_NEW;
// Do not send notification if conversation is spam.
if ($event->conversation->status != Conversation::STATUS_SPAM) {
$event_type = Subscription::EVENT_TYPE_NEW;
}
break;
case 'App\Events\ConversationUserChanged':
$caused_by_user_id = $event->user->id;
@@ -52,12 +62,13 @@ class SendNotificationToUsers
if (empty($event->conversation) || !$event_type) {
return;
}
// Ignore imported threads.
if (!empty($event->thread) && $event->thread->imported) {
return;
}
$conversation = $event->conversation;
// We can not check imported here, as after conversation has been imported via API
// notifications has to be sent.
//if (!$conversation->imported) {
// Using the last argument you can make event to be processed immediately
Subscription::registerEvent($event_type, $conversation, $caused_by_user_id/*, true*/);
}

View File

@@ -23,11 +23,31 @@ class SendReplyToCustomer
{
$conversation = $event->conversation;
// Do not send email if this is a Phone conversation.
if ($conversation->isPhone()) {
return;
}
$replies = $conversation->getReplies();
// Ignore imported messages.
if ($replies && $replies->first() && $replies->first()->imported) {
return;
}
// Chat conversation.
if ($conversation->isChat()) {
\Helper::backgroundAction('chat_conversation.send_reply', [$conversation, $replies, $conversation->customer], now()->addSeconds(Conversation::UNDO_TIMOUT));
return;
}
// We can not check imported here, as after conversation has been imported via API
// notifications has to be sent.
//if (!$conversation->imported) {
\App\Jobs\SendReplyToCustomer::dispatch($conversation, $conversation->getReplies(), $conversation->customer)
->delay(now()->addSeconds(Conversation::UNDO_TIMOUT))
$delay = \Eventy::filter('conversation.send_reply_to_customer_delay', now()->addSeconds(Conversation::UNDO_TIMOUT), $conversation, $replies);
\App\Jobs\SendReplyToCustomer::dispatch($conversation, $replies, $conversation->customer)
->delay($delay)
->onQueue('emails');
}
}

View File

@@ -2,8 +2,6 @@
namespace App\Mail;
use Illuminate\Container\Container;
use Illuminate\Contracts\Mail\Mailer as MailerContract;
use Illuminate\Mail\Mailable;
class Alert extends Mailable
@@ -20,7 +18,6 @@ class Alert extends Mailable
/**
* Create a new message instance.
*
*/
public function __construct($text, $title = '')
{

View File

@@ -2,41 +2,32 @@
namespace App\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Container\Container;
use Illuminate\Contracts\Mail\Mailer as MailerContract;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
class AutoReply extends Mailable
{
/**
* Conversation created by customer.
*
*/
public $conversation;
/**
* Mailbox.
*
*/
public $mailbox;
/**
* Customer.
*
*/
public $customer;
/**
* Custom headers.
*
*/
public $headers = [];
/**
* Create a new message instance.
*
*/
public function __construct($conversation, $mailbox, $customer, $headers)
{
@@ -65,8 +56,10 @@ class AutoReply extends Mailable
];
// Set variables
$subject = \App\Misc\Mail::replaceMailVars($this->mailbox->auto_reply_subject, $data);
$view_params['auto_reply_message'] = \App\Misc\Mail::replaceMailVars($this->mailbox->auto_reply_message, $data);
$subject = \MailHelper::replaceMailVars($this->mailbox->auto_reply_subject, $data);
$view_params['auto_reply_message'] = \MailHelper::replaceMailVars($this->mailbox->auto_reply_message, $data);
$subject = \Eventy::filter('email.auto_reply.subject', $subject, $this->conversation);
$message = $this->subject($subject)
->view('emails/customer/auto_reply', $view_params)

View File

@@ -2,12 +2,7 @@
namespace App\Mail;
use App\Option;
use Illuminate\Bus\Queueable;
use Illuminate\Container\Container;
use Illuminate\Contracts\Mail\Mailer as MailerContract;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
class PasswordChanged extends Mailable
{
@@ -18,7 +13,6 @@ class PasswordChanged extends Mailable
/**
* Create a new message instance.
*
*/
public function __construct($user)
{

View File

@@ -2,11 +2,9 @@
namespace App\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Container\Container;
use Illuminate\Contracts\Mail\Mailer as MailerContract;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
// https://medium.com/@guysmilez/queuing-mailables-with-custom-headers-in-laravel-5-4-ab615f022f17
//abstract class AbstractMessage extends Mailable
@@ -83,6 +81,7 @@ class ReplyToCustomer extends Mailable
if (count($this->threads) > 1) {
$subject = 'Re: '.$subject;
}
$subject = \Eventy::filter('email.reply_to_customer.subject', $subject, $this->conversation);
// from($this->from) Sets only email, name stays empty.
// So we set from in Mail::setMailDriver
@@ -92,7 +91,7 @@ class ReplyToCustomer extends Mailable
if ($this->threads->first()->has_attachments) {
foreach ($this->threads->first()->attachments as $attachment) {
$message->attach($attachment->getLocalFilePath());
$message->attach($attachment->getLocalFilePath());
}
}

View File

@@ -2,8 +2,6 @@
namespace App\Mail;
use Illuminate\Container\Container;
use Illuminate\Contracts\Mail\Mailer as MailerContract;
use Illuminate\Mail\Mailable;
class Test extends Mailable
@@ -12,7 +10,6 @@ class Test extends Mailable
/**
* Create a new message instance.
*
*/
public function __construct($mailbox = null)
{
@@ -26,6 +23,13 @@ class Test extends Mailable
*/
public function build()
{
$this->withSwiftMessage(function ($swiftmessage) {
$headers = $swiftmessage->getHeaders();
$headers->addTextHeader('X-FreeScout-Mail-Type', 'test.mailbox');
return $swiftmessage;
});
$message = $this->subject(__(':app_name Test Email', ['app_name' => \Config::get('app.name')]));
if ($this->mailbox) {
$message->view('emails/user/test', ['mailbox' => $this->mailbox]);

View File

@@ -2,11 +2,10 @@
/**
* User replied from wrong email address to the email notification.
*/
namespace App\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
class UserEmailReplyError extends Mailable
{
@@ -17,7 +16,6 @@ class UserEmailReplyError extends Mailable
*/
public function __construct()
{
}
/**

View File

@@ -3,11 +3,7 @@
namespace App\Mail;
use App\Option;
use Illuminate\Bus\Queueable;
use Illuminate\Container\Container;
use Illuminate\Contracts\Mail\Mailer as MailerContract;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
class UserInvite extends Mailable
{
@@ -18,7 +14,6 @@ class UserInvite extends Mailable
/**
* Create a new message instance.
*
*/
public function __construct($user)
{
@@ -32,7 +27,7 @@ class UserInvite extends Mailable
*/
public function build()
{
$message = $this->subject(__('Welcome to :company_name', ['company_name' => Option::get('company_name')]))
$message = $this->subject(__('Welcome to :company_name!', ['company_name' => Option::getCompanyName()]))
->view('emails/user/user_invite')
->text('emails/user/user_invite_text');

View File

@@ -2,9 +2,7 @@
namespace App\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
class UserNotification extends Mailable
{
@@ -45,7 +43,7 @@ class UserNotification extends Mailable
/**
* Mailbox.
*
*
* @var [type]
*/
public $mailbox;

View File

@@ -2,11 +2,18 @@
namespace App;
use App\Email;
use App\User;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Hash;
use Watson\Rememberable\Rememberable;
class Mailbox extends Model
{
use Rememberable;
// This is obligatory.
public $rememberCacheDriver = 'array';
/**
* From Name: name that will appear in the From field when a customer views your email.
*/
@@ -86,11 +93,33 @@ class Mailbox extends Model
const RATINGS_PLACEMENT_ABOVE = 1;
const RATINGS_PLACEMENT_BELOW = 2;
/**
* Access permissions.
*/
const ACCESS_PERM_EDIT = 'edit';
const ACCESS_PERM_PERMISSIONS = 'perm';
const ACCESS_PERM_AUTO_REPLIES = 'auto';
const ACCESS_PERM_SIGNATURE = 'sig';
public static $access_permissions = [
self::ACCESS_PERM_EDIT,
self::ACCESS_PERM_PERMISSIONS,
self::ACCESS_PERM_AUTO_REPLIES,
self::ACCESS_PERM_SIGNATURE,
];
public static $access_routes = [
self::ACCESS_PERM_EDIT => 'mailboxes.update',
self::ACCESS_PERM_PERMISSIONS => 'mailboxes.permissions',
self::ACCESS_PERM_AUTO_REPLIES => 'mailboxes.auto_reply',
self::ACCESS_PERM_SIGNATURE => 'mailboxes.update',
];
/**
* Default signature set when mailbox created.
*/
const DEFAULT_SIGNATURE = '<br><span style="color:#808080;">--<br>
{%mailbox.name%}<br></span>';
{%mailbox.name%}</span>';
/**
* Default values.
@@ -99,20 +128,82 @@ class Mailbox extends Model
'signature' => self::DEFAULT_SIGNATURE,
];
protected $casts = [
'meta' => 'array',
];
/**
* Attributes fillable using fill() method.
*
* @var [type]
*/
protected $fillable = ['name', 'slug', 'email', 'aliases', 'from_name', 'from_name_custom', 'ticket_status', 'ticket_assignee', 'template', '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', 'auto_reply_enabled', 'auto_reply_subject', 'auto_reply_message', 'office_hours_enabled', 'ratings', 'ratings_placement', 'ratings_text'];
protected $fillable = ['name', 'email', 'aliases', '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 static function boot()
{
parent::boot();
self::created(function (Mailbox $model) {
$model->slug = strtolower(substr(md5(Hash::make($model->id)), 0, 16));
});
// self::created(function (Mailbox $model) {
// $model->slug = strtolower(substr(md5(Hash::make($model->id)), 0, 16));
// });
}
/**
* Automatically encrypt password on save.
*/
public function setInPasswordAttribute($value)
{
if ($value != '') {
$this->attributes['in_password'] = encrypt($value);
} else {
$this->attributes['in_password'] = '';
}
}
/**
* Automatically decrypt password on read.
*/
public function getInPasswordAttribute($value)
{
if (!$value) {
return '';
}
try {
return decrypt($value);
} catch (\Exception $e) {
// do nothing if decrypt wasn't succefull
return '';
}
}
/**
* Automatically encrypt password on save.
*/
public function setOutPasswordAttribute($value)
{
if ($value != '') {
$this->attributes['out_password'] = encrypt($value);
} else {
$this->attributes['out_password'] = '';
}
}
/**
* Automatically decrypt password on read.
*/
public function getOutPasswordAttribute($value)
{
if (!$value) {
return '';
}
try {
return decrypt($value);
} catch (\Exception $e) {
// do nothing if decrypt wasn't succefull
return '';
}
}
/**
@@ -120,7 +211,24 @@ class Mailbox extends Model
*/
public function users()
{
return $this->belongsToMany('App\User')->as('settings')->withPivot('after_send');
return $this->belongsToMany('App\User');
}
public function usersWithSettings()
{
return $this->belongsToMany('App\User')->as('settings')
->withPivot('after_send')
->withPivot('hide')
->withPivot('mute')
->withPivot('access');
}
/**
* Get users having access to the mailbox.
*/
public function users_cached()
{
return $this->users()->rememberForever();
}
/**
@@ -176,11 +284,11 @@ class Mailbox extends Model
continue;
}
foreach ($folder_types as $type) {
$folder = new Folder();
$folder->mailbox_id = $mailbox_id;
$folder->user_id = $user_id;
$folder->type = $type;
$folder->save();
Folder::create([
'mailbox_id' => $mailbox_id,
'user_id' => $user_id,
'type' => $type,
]);
}
}
}
@@ -217,26 +325,45 @@ class Mailbox extends Model
*/
public function getMainFolders()
{
$main_folders = \Eventy::filter('mailbox.main_folders', [], $this);
if ($main_folders) {
return $main_folders;
}
return $this->folders()
->where(function ($query) {
$query->whereIn('type', [Folder::TYPE_UNASSIGNED, Folder::TYPE_ASSIGNED, Folder::TYPE_DRAFTS])
->orWhere(function ($query2) {
$query2->where(['type' => Folder::TYPE_MINE]);
$query2->where(['user_id' => auth()->user()->id]);
})
->orWhere(function ($query3) {
$query3->where(['type' => Folder::TYPE_STARRED]);
$query3->where(['user_id' => auth()->user()->id]);
});
})
->orderBy('type')
->get();
}
/**
* Get folder by it's type.
*/
public function getFolderByType($type)
{
return $this->folders()
->where('type', $type)
->first();
}
/**
* Get folders available for the current user.
*/
public function getAssesibleFolders()
{
return $this->folders()
$folders = $this->folders()
->where(function ($query) {
$query->whereIn('type', Folder::$public_types)
$query->whereIn('type', \Eventy::filter('mailbox.folders.public_types', Folder::$public_types))
->orWhere(function ($query2) {
$query2->whereIn('type', Folder::$personal_types);
$query2->where(['user_id' => auth()->user()->id]);
@@ -244,6 +371,8 @@ class Mailbox extends Model
})
->orderBy('type')
->get();
return \Eventy::filter('mailbox.folders', $folders, $this);
}
/**
@@ -279,6 +408,12 @@ class Mailbox extends Model
*/
public function isInActive()
{
$in_active = \Eventy::filter('mailbox.in_active', null, $this);
if (is_bool($in_active)) {
return $in_active;
}
if ($this->in_protocol && $this->in_server && $this->in_port && $this->in_username && $this->in_password) {
return true;
} else {
@@ -294,7 +429,7 @@ class Mailbox extends Model
public function isOutActive()
{
if ($this->out_method != self::OUT_METHOD_PHP_MAIL && $this->out_method != self::OUT_METHOD_SENDMAIL
&& (!$this->out_server || !$this->out_username || !$this->out_password)
&& (!$this->out_server /*|| !$this->out_username || !$this->out_password*/)
) {
return false;
} else {
@@ -305,12 +440,59 @@ class Mailbox extends Model
/**
* Get users who have access to the mailbox.
*/
public function usersHavingAccess($cache = false)
public function usersHavingAccess($cache = false, $fields = 'users.*', $sort = true)
{
$users = $this->users;
$admins = User::where('role', User::ROLE_ADMIN)->remember(\App\Misc\Helper::cacheTime($cache))->get();
$admins = User::where('role', User::ROLE_ADMIN)->select($fields)->remember(\Helper::cacheTime($cache))->get();
return $users->merge($admins)->unique();
$users = $this->users()->select($fields)->remember(\Helper::cacheTime($cache))->get()->merge($admins)->unique();
// Exclude deleted users (better to do it in PHP).
foreach ($users as $i => $user) {
if (!$user->isActive()) {
$users->forget($i);
}
}
// Sort by full name
if ($sort) {
$users = User::sortUsers($users);
}
return $users;
}
public function usersAssignable($cache = true)
{
// Exclude hidden admins.
$mailbox_id = $this->id;
$admins = User::select(['users.*', 'mailbox_user.hide'])
->leftJoin('mailbox_user', function ($join) use ($mailbox_id) {
$join->on('mailbox_user.user_id', '=', 'users.id');
$join->where('mailbox_user.mailbox_id', $mailbox_id);
})
->where('role', User::ROLE_ADMIN)
->remember(\Helper::cacheTime($cache))
->get();
$users = $this->users()->select('users.*')->remember(\Helper::cacheTime($cache))->get()->merge($admins)->unique();
foreach ($users as $i => $user) {
if (!empty($user->hide)) {
$users->forget($i);
}
}
// Exclude deleted users (better to do it in PHP).
foreach ($users as $i => $user) {
if (!$user->isActive()) {
$users->forget($i);
}
}
// Sort by full name
$users = User::sortUsers($users);
return $users;
}
/**
@@ -318,10 +500,12 @@ class Mailbox extends Model
*/
public function userIdsHavingAccess()
{
$user_ids = $this->users()->pluck('users.id');
return $this->usersHavingAccess(false, ['users.id', 'users.status'])->pluck('id')->toArray();
/*$user_ids = $this->users()->pluck('users.id');
$admin_ids = User::where('role', User::ROLE_ADMIN)->pluck('id');
return $user_ids->merge($admin_ids)->unique()->toArray();
return $user_ids->merge($admin_ids)->unique()->toArray();*/
}
/**
@@ -329,9 +513,15 @@ class Mailbox extends Model
*
* @return bool
*/
public function userHasAccess($user_id)
public function userHasAccess($user_id, $user = null)
{
$user = User::find($user_id);
if (!$user) {
if ($user_id instanceof \App\User) {
$user = $user_id;
} else {
$user = User::find($user_id);
}
}
if ($user && $user->isAdmin()) {
return true;
} else {
@@ -417,7 +607,32 @@ class Mailbox extends Model
*/
public function getInProtocolName()
{
return self::$in_protocols[$this->in_protocol];
return $this->getInProtocols()[$this->in_protocol] ?? '';
}
/**
* Get incoming protocol display name for the UI.
*
* @return array
*/
public static function getInProtocolDisplayNames()
{
$display_names = self::$in_protocols;
$display_names[self::IN_PROTOCOL_IMAP] = 'IMAP';
$display_names[self::IN_PROTOCOL_POP3] = 'POP3';
return \Eventy::filter('mailbox.in_protocols.display_names', $display_names);
}
/**
* Get available incoming protocols.
*
* @return array
*/
public static function getInProtocols()
{
return \Eventy::filter('mailbox.in_protocols', self::$in_protocols);
}
/**
@@ -425,22 +640,43 @@ class Mailbox extends Model
*/
public function getUserSettings($user_id)
{
$mailbox_user = $this->users()->where('users.id', $user_id)->first();
$mailbox_user = $this->usersWithSettings()->where('users.id', $user_id)->first();
if ($mailbox_user) {
return $mailbox_user->settings;
} else {
// Admin may have no record in mailbox_user table
// Create dummy object with default parameters
$settings = new \StdClass();
$settings->after_send = MailboxUser::AFTER_SEND_NEXT;
return $settings;
return self::getDummySettings();
}
}
/**
* Create dummy object with default parameters
* @return [type] [description]
*/
public static function getDummySettings()
{
$settings = new \StdClass();
$settings->after_send = MailboxUser::AFTER_SEND_NEXT;
$settings->hide = false;
$settings->mute = false;
$settings->access = [];
return $settings;
}
public function fetchUserSettings($user_id)
{
$settings = $this->getUserSettings($user_id);
$this->after_send = $settings->after_send;
$this->hide = $settings->hide;
$this->mute = $settings->mute;
$this->access = $settings->access;
}
/**
* Get main email and aliases.
*
*
* @return array
*/
public function getEmails()
@@ -456,13 +692,14 @@ class Mailbox extends Model
}
}
}
return $emails;
}
/**
* Remove mailbox email and aliases from the list of emails.
*
* @param array $list
* @param array $list
* @param Mailbox $mailbox
*
* @return array
@@ -481,4 +718,173 @@ class Mailbox extends Model
return $list;
}
/**
* Get all active mailboxes.
*
* @return [type] [description]
*/
public static function getActiveMailboxes()
{
$active = [];
// It is more effective to retrive all mailboxes and filter them in PHP.
$mailboxes = self::all();
foreach ($mailboxes as $mailbox) {
if ($mailbox->isActive()) {
$active[] = $mailbox;
}
}
return $active;
}
/**
* Get mailbox URL.
*
* @return [type] [description]
*/
public function url()
{
return \Eventy::filter('mailbox.url', route('mailboxes.view', ['id' => $this->id]), $this);
}
/**
* Fill the model with an array of attributes.
*
* @param array $attributes [description]
*
* @return [type] [description]
*/
public function fill(array $attributes)
{
$this->fillable(array_merge($this->getFillable(), \Eventy::filter('mailbox.fillable_fields', [])));
return parent::fill($attributes);
}
/**
* Set phones as JSON.
*
* @param array $phones_array
*/
public function setInImapFolders(array $in_imap_folders)
{
$this->in_imap_folders = json_encode($in_imap_folders);
}
/**
* Get list of imap folders.
*/
public function getInImapFolders()
{
$in_imap_folders = \Helper::jsonToArray($this->in_imap_folders);
if (count($in_imap_folders)) {
return $in_imap_folders;
} else {
return ["INBOX"];
}
}
public function outPasswordSafe()
{
return \Helper::safePassword($this->out_password);
}
public function inPasswordSafe()
{
return \Helper::safePassword($this->in_password);
}
public function getReplySeparator()
{
return $this->before_reply ?: \MailHelper::REPLY_SEPARATOR_TEXT;
}
public static function findOrFailWithSettings($id, $user_id)
{
return Mailbox::select(['mailboxes.*', 'mailbox_user.hide', 'mailbox_user.mute', 'mailbox_user.access'])
->where('mailboxes.id', $id)
->leftJoin('mailbox_user', function ($join) use ($user_id) {
$join->on('mailbox_user.mailbox_id', '=', 'mailboxes.id');
$join->where('mailbox_user.user_id', $user_id);
})->firstOrFail();
}
/*public static function getUserSettings($mailbox_id, $user_id)
{
return MailboxUser::where('mailbox_id', $mailbox_id)
->where('user_id', $user_id)
->first();
}*/
public static function getAccessPermissionName($perm)
{
$access_permissions = [
self::ACCESS_PERM_EDIT => __('Edit Mailbox'),
self::ACCESS_PERM_PERMISSIONS => __('Permissions'),
self::ACCESS_PERM_AUTO_REPLIES => __('Auto Replies'),
self::ACCESS_PERM_SIGNATURE => __('Email Signature'),
];
$access_permissions = \Eventy::filter('mailbox.access_permissions_list', $access_permissions);
return $access_permissions[$perm] ?? '';
}
public static function getAccessPermissionRoute($perm)
{
$route = self::$access_routes[$perm] ?? '';
$route = \Eventy::filter('mailbox.access_permissions_route', $route, $perm);
return $route;
}
public function setMetaParam($param, $value, $save = false)
{
$meta = $this->meta;
$meta[$param] = $value;
$this->meta = $meta;
if ($save) {
$this->save();
}
}
public function removeMetaParam($param, $save = false)
{
$meta = $this->meta;
if (isset($meta[$param])) {
unset($meta[$param]);
}
$this->meta = $meta;
if ($save) {
$this->save();
}
}
/**
* Check if there is a user with specified email.
*/
public static function userEmailExists($email)
{
$email = Email::sanitizeEmail($email);
$user = User::where('email', $email)->first();
if ($user) {
return true;
} else {
return false;
}
}
public function oauthEnabled()
{
return !empty($this->meta['oauth']['provider']);
}
public function oauthGetParam($param)
{
return $this->meta['oauth'][$param] ?? '';
}
}

View File

@@ -3,14 +3,25 @@
namespace App;
use Illuminate\Database\Eloquent\Model;
use Watson\Rememberable\Rememberable;
class MailboxUser extends Model
{
use Rememberable;
// This is obligatory.
public $rememberCacheDriver = 'array';
// Action after sending a message
const AFTER_SEND_STAY = 1;
const AFTER_SEND_NEXT = 2;
const AFTER_SEND_FOLDER = 3;
protected $table = 'mailbox_user';
public $timestamps = false;
// Does not work as we receive it via pivot
// protected $casts = [
// 'access' => 'array',
// ];
}

File diff suppressed because it is too large Load Diff

View File

@@ -35,22 +35,50 @@ class Mail
* Encryptions.
*/
const MAIL_ENCRYPTION_NONE = '';
const MAIL_ENCRYPTION_SSL = 'ssl';
const MAIL_ENCRYPTION_TLS = 'tls';
const MAIL_ENCRYPTION_SSL = 'ssl';
const MAIL_ENCRYPTION_TLS = 'tls';
const FETCH_SCHEDULE_EVERY_MINUTE = 1;
const FETCH_SCHEDULE_EVERY_FIVE_MINUTES = 5;
const FETCH_SCHEDULE_EVERY_TEN_MINUTES = 10;
const FETCH_SCHEDULE_EVERY_FIFTEEN_MINUTES = 15;
const FETCH_SCHEDULE_EVERY_THIRTY_MINUTES = 30;
const FETCH_SCHEDULE_HOURLY = 60;
const OAUTH_PROVIDER_MICROSOFT = 'ms';
/**
* If reply is not extracted properly from the incoming email, add here new separator.
* If reply is not extracted properly from the incoming email, add here a new separator.
* Order is not important.
* Idially separators must contain < or > to avoid false positives.
* Regex separators has "regex:" in the beginning.
*/
public static $alternative_reply_separators = [
self::REPLY_SEPARATOR_HTML,
self::REPLY_SEPARATOR_TEXT,
'<div class="gmail_quote">',
'<blockquote',
self::REPLY_SEPARATOR_HTML, // Our HTML separator
self::REPLY_SEPARATOR_TEXT, // Our plain text separator
// Email service providers specific separators.
'<div class="gmail_quote">', // Gmail
'<div id="appendonsend"></div>', // Outlook / Live / Hotmail / Microsoft
'<div name="quote" ',
'yahoo_quoted_', // Yahoo, full: <div id=3D"ydp6h4f5c59yahoo_quoted_2937493705"
'------------------ 原始邮件 ------------------', // QQ
'------------------ Original ------------------', // QQ English
'<div id=3D"divRplyFwdMsg" dir=', // Outlook
'regex:/<div style="border:none;border\-top:solid \#[A-Z0-9]{6} 1\.0pt;padding:3\.0pt 0in 0in 0in">[^<]*<p class="MsoNormal"><b>/', // MS Outlook
// General separators.
'regex:/<blockquote((?!quote)[^>])*>/', // General sepator. Should skip Gmail's <blockquote class="gmail_quote">.
'<!-- originalMessage -->',
//'---Original---', // QQ separator, wait for emails from QQ and check
'‐‐‐‐‐‐‐ Original Message ‐‐‐‐‐‐‐',
'--------------- Original Message ---------------',
];
/**
* md5 of the last applied mail config.
*/
public static $last_mail_config_hash = '';
/**
* Configure mail sending parameters.
*
@@ -67,8 +95,13 @@ class Mail
if ($mailbox->out_method == Mailbox::OUT_METHOD_SMTP) {
\Config::set('mail.host', $mailbox->out_server);
\Config::set('mail.port', $mailbox->out_port);
\Config::set('mail.username', $mailbox->out_username);
\Config::set('mail.password', $mailbox->out_password);
if (!$mailbox->out_username) {
\Config::set('mail.username', null);
\Config::set('mail.password', null);
} else {
\Config::set('mail.username', $mailbox->out_username);
\Config::set('mail.password', $mailbox->out_password);
}
\Config::set('mail.encryption', $mailbox->getOutEncryptionName());
}
} else {
@@ -77,7 +110,33 @@ class Mail
\Config::set('mail.from', ['address' => self::getSystemMailFrom(), 'name' => '']);
}
self::reapplyMailConfig();
}
/**
* Reapply new mail config.
*/
public static function reapplyMailConfig()
{
// Check hash to avoid recreating MailServiceProvider.
$mail_config_hash = md5(json_encode(\Config::get('mail')));
if (self::$last_mail_config_hash != $mail_config_hash) {
self::$last_mail_config_hash = $mail_config_hash;
} else {
return false;
}
// Without doing this, Swift mailer uses old config values
// if there were emails sent with previous config.
\App::forgetInstance('mailer');
\App::forgetInstance('swift.mailer');
\App::forgetInstance('swift.transport');
(new \Illuminate\Mail\MailServiceProvider(app()))->register();
// We have to update Mailer facade manually, as it does not happen automatically
// and previous instance of app('mailer') is used.
\Mail::swap(app('mailer'));
}
/**
@@ -89,40 +148,66 @@ class Mail
{
\Config::set('mail.driver', self::getSystemMailDriver());
\Config::set('mail.from', [
'address' => self::getSystemMailFrom(),
'name' => Option::get('company_name', \Config::get('app.name'))
'address' => self::getSystemMailFrom(),
'name' => Option::get('company_name', \Config::get('app.name')),
]);
// SMTP
if (\Config::get('mail.driver') == self::MAIL_DRIVER_SMTP) {
\Config::set('mail.host', Option::get('mail_host'));
\Config::set('mail.port', Option::get('mail_port'));
\Config::set('mail.username', Option::get('mail_username'));
\Config::set('mail.password', Option::get('mail_password'));
if (!Option::get('mail_username')) {
\Config::set('mail.username', null);
\Config::set('mail.password', null);
} else {
\Config::set('mail.username', Option::get('mail_username'));
\Config::set('mail.password', \Helper::decrypt(Option::get('mail_password')));
}
\Config::set('mail.encryption', Option::get('mail_encryption'));
}
(new \Illuminate\Mail\MailServiceProvider(app()))->register();
self::reapplyMailConfig();
}
/**
* Replace mail vars in the text.
*/
public static function replaceMailVars($text, $data = [])
public static function replaceMailVars($text, $data = [], $escape = false)
{
// Available variables to insert into email in UI.
$vars = [
'{%subject%}' => $data['conversation']->subject,
'{%mailbox.email%}' => $data['mailbox']->email,
'{%mailbox.name%}' => $data['mailbox']->name,
'{%conversation.number%}' => $data['conversation']->number,
'{%customer.email%}' => $data['conversation']->customer_email
];
$vars = [];
if ($data['customer']) {
$vars['{%customer.fullName%}'] = $data['customer']->getFullName(true);
if (!empty($data['conversation'])) {
$vars['{%subject%}'] = $data['conversation']->subject;
$vars['{%conversation.number%}'] = $data['conversation']->number;
$vars['{%customer.email%}'] = $data['conversation']->customer_email;
}
if (!empty($data['mailbox'])) {
$vars['{%mailbox.email%}'] = $data['mailbox']->email;
$vars['{%mailbox.name%}'] = $data['mailbox']->name;
$vars['{%mailbox.fromName%}'] = $data['mailbox']->getMailFrom(!empty($data['user']) ? $data['user'] : null)['name'];
}
if (!empty($data['customer'])) {
$vars['{%customer.fullName%}'] = $data['customer']->getFullName(true);
$vars['{%customer.firstName%}'] = $data['customer']->getFirstName(true);
$vars['{%customer.lastName%}'] = $data['customer']->last_name;
$vars['{%customer.lastName%}'] = $data['customer']->last_name;
}
if (!empty($data['user'])) {
$vars['{%user.fullName%}'] = $data['user']->getFullName();
$vars['{%user.firstName%}'] = $data['user']->getFirstName();
$vars['{%user.phone%}'] = $data['user']->phone;
$vars['{%user.email%}'] = $data['user']->email;
$vars['{%user.jobTitle%}'] = $data['user']->job_title;
$vars['{%user.lastName%}'] = $data['user']->last_name;
$vars['{%user.photoUrl%}'] = $data['user']->getPhotoUrl();
}
$vars = \Eventy::filter('mail_vars.replace', $vars, $data);
if ($escape) {
foreach ($vars as $i => $var) {
$vars[$i] = htmlspecialchars($var ?? '');
}
}
return strtr($text, $vars);
@@ -133,7 +218,7 @@ class Mail
*/
public static function hasVars($text)
{
return preg_match("/({%|%})/", $text);
return preg_match('/({%|%})/', $text);
}
/**
@@ -153,6 +238,7 @@ class Mail
if (!$mail_from) {
$mail_from = 'freescout@'.\Helper::getDomain();
}
return $mail_from;
}
@@ -174,6 +260,7 @@ class Mail
\MailHelper::setMailDriver($mailbox);
$status_message = '';
try {
\Mail::to([$to])->send(new \App\Mail\Test($mailbox));
} catch (\Exception $e) {
@@ -185,6 +272,7 @@ class Mail
\MailHelper::setSystemMailDriver();
$status_message = '';
try {
\Mail::to([['name' => '', 'email' => $to]])
->send(new \App\Mail\Test());
@@ -203,6 +291,7 @@ class Mail
}
} else {
SendLog::log(null, null, $to, SendLog::MAIL_TYPE_TEST, SendLog::STATUS_ACCEPTED);
return true;
}
}
@@ -212,15 +301,7 @@ class Mail
*/
public static function fetchTest($mailbox)
{
$client = new Client([
'host' => $mailbox->in_server,
'port' => $mailbox->in_port,
'encryption' => $mailbox->getInEncryptionName(),
'validate_cert' => true,
'username' => $mailbox->in_username,
'password' => $mailbox->in_password,
'protocol' => $mailbox->getInProtocolName(),
]);
$client = \MailHelper::getMailboxClient($mailbox);
// Connect to the Server
$client->connect();
@@ -234,8 +315,24 @@ class Mail
// Get unseen messages for a period
$messages = $folder->query()->unseen()->since(now()->subDays(1))->leaveUnread()->get();
if ($client->getLastError()) {
throw new \Exception($client->getLastError(), 1);
$last_error = '';
if (method_exists($client, 'getLastError')) {
$last_error = $client->getLastError();
}
if ($last_error && stristr($last_error, 'The specified charset is not supported')) {
// Solution for MS mailboxes.
// https://github.com/freescout-helpdesk/freescout/issues/176
$messages = $folder->query()->unseen()->since(now()->subDays(1))->leaveUnread()->setCharset(null)->get();
if (count($client->getErrors()) > 1) {
$last_error = $client->getLastError();
} else {
$last_error = null;
}
}
if ($last_error) {
throw new \Exception($last_error, 1);
} else {
return true;
}
@@ -253,7 +350,7 @@ class Mail
if (is_array($emails)) {
$emails_array = $emails;
} else {
$emails_array = explode(',', $emails);
$emails_array = explode(',', $emails ?? '');
}
foreach ($emails_array as $i => $email) {
@@ -266,6 +363,18 @@ class Mail
return $emails_array;
}
/**
* Check if email format is valid.
*
* @param [type] $email [description]
*
* @return [type] [description]
*/
public static function validateEmail($email)
{
return filter_var($email, FILTER_VALIDATE_EMAIL);
}
/**
* Send system alert to super admin.
*/
@@ -283,8 +392,9 @@ class Mail
\MailHelper::setSystemMailDriver();
$status_message = '';
try {
\Mail::raw($body, function($message) use ($subject, $attachments, $from_user) {
\Mail::raw($body, function ($message) use ($subject, $attachments, $from_user) {
$message
->subject($subject)
->to(\Config::get('app.freescout_email'));
@@ -310,4 +420,400 @@ class Mail
return true;
}
}
/**
* Get email marker for the outgoing email to track replies
* in case Message-ID header is removed by mail service provider.
*
* @param [type] $message_id [description]
*
* @return [type] [description]
*/
public static function getMessageMarker($message_id)
{
// It has to be BASE64, as Gmail converts it into link.
return '{#FS:'.base64_encode($message_id).'#}';
}
/**
* Fetch Message-ID from incoming email body.
*
* @param [type] $message_id [description]
*
* @return [type] [description]
*/
public static function fetchMessageMarkerValue($body)
{
preg_match('/{#FS:([^#]+)#}/', $body, $matches);
if (!empty($matches[1]) && base64_decode($matches[1])) {
// Return first found marker.
return base64_decode($matches[1]);
}
return '';
}
/**
* Detect autoresponder by headers.
* https://github.com/jpmckinney/multi_mail/wiki/Detecting-autoresponders
* https://www.jitbit.com/maxblog/18-detecting-outlook-autoreplyout-of-office-emails-and-x-auto-response-suppress-header/.
*
* @return bool [description]
*/
public static function isAutoResponder($headers_str)
{
$autoresponder_headers = [
'x-autoreply' => '',
'x-autorespond' => '',
'auto-submitted' => 'auto-replied',
'precedence' => ['auto_reply', 'bulk', 'junk'],
'x-precedence' => ['auto_reply', 'bulk', 'junk'],
];
$headers = explode("\n", $headers_str ?? '');
foreach ($autoresponder_headers as $auto_header => $auto_header_value) {
foreach ($headers as $header) {
$parts = explode(':', $header, 2);
if (count($parts) == 2) {
$name = trim(strtolower($parts[0]));
$value = trim($parts[1]);
} else {
continue;
}
if (strtolower($name) == $auto_header) {
if (!$auto_header_value) {
return true;
} elseif (is_array($auto_header_value)) {
foreach ($auto_header_value as $auto_header_value_item) {
if ($value == $auto_header_value_item) {
return true;
}
}
} elseif ($value == $auto_header_value) {
return true;
}
}
}
}
return false;
}
/**
* Check Content-Type header.
* This is not 100% reliable, detects only standard DSN bounces.
*
* @param [type] $headers [description]
*
* @return [type] [description]
*/
public static function detectBounceByHeaders($headers)
{
if (preg_match("/Content-Type:((?:[^\n]|\n[\t ])+)(?:\n[^\t ]|$)/i", $headers, $match)
&& preg_match("/multipart\/report/i", $match[1])
&& preg_match("/report-type=[\"']?delivery-status[\"']?/i", $match[1])
) {
return true;
} else {
return false;
}
}
/**
* Parse email headers.
*
* @param [type] $headers_str [description]
*
* @return [type] [description]
*/
public static function parseHeaders($headers_str)
{
try {
return imap_rfc822_parse_headers($headers_str);
} catch (\Exception $e) {
return;
}
}
public static function getHeader($headers_str, $header)
{
$headers = self::parseHeaders($headers_str);
if (!$headers) {
return;
}
$value = null;
if (property_exists($headers, $header)) {
$value = $headers->$header;
} else {
return;
}
switch ($header) {
case 'message_id':
$value = str_replace(['<', '>'], '', $value);
break;
}
return $value;
}
/**
* Get client for fetching emails.
*/
public static function getMailboxClient($mailbox)
{
if (!$mailbox->oauthEnabled()) {
return new \Webklex\IMAP\Client([
'host' => $mailbox->in_server,
'port' => $mailbox->in_port,
'encryption' => $mailbox->getInEncryptionName(),
'validate_cert' => $mailbox->in_validate_cert,
'username' => $mailbox->in_username,
'password' => $mailbox->in_password,
'protocol' => $mailbox->getInProtocolName(),
]);
} else {
\Config::set('imap.accounts.default', [
'host' => $mailbox->in_server,
'port' => $mailbox->in_port,
'encryption' => $mailbox->getInEncryptionName(),
'validate_cert' => $mailbox->in_validate_cert,
'username' => $mailbox->email,
'password' => $mailbox->oauthGetParam('a_token'),
'protocol' => $mailbox->getInProtocolName(),
'authentication' => 'oauth',
]);
// To enable debug: /vendor/webklex/php-imap/src/Connection/Protocols
// Debug in console
if (app()->runningInConsole()) {
\Config::set('imap.options.debug', config('app.debug'));
}
$cm = new \Webklex\PHPIMAP\ClientManager(config('imap'));
// Refresh Access Token.
if ((strtotime($mailbox->oauthGetParam('issued_on')) + (int)$mailbox->oauthGetParam('expires_in')) < time()) {
// 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,
'refresh_token' => $mailbox->oauthGetParam('r_token'),
]);
if (!empty($token_data['a_token'])) {
$mailbox->setMetaParam('oauth', $token_data, true);
} elseif (!empty($token_data['error'])) {
$error_message = 'Error occurred refreshing oAuth Access Token: '.$token_data['error'];
\Helper::log(\App\ActivityLog::NAME_EMAILS_FETCHING,
\App\ActivityLog::DESCRIPTION_EMAILS_FETCHING_ERROR, [
'error' => $error_message,
'mailbox' => $mailbox->name,
]);
throw new \Exception($error_message, 1);
}
}
// This makes it authenticate two times.
//$cm->setTimeout(60);
return $cm->account('default');
}
}
/**
* Generate artificial Message-ID.
*/
public static function generateMessageId($email_address, $raw_body = '')
{
$hash = str_random(16);
if ($raw_body) {
$hash = md5(strval($raw_body));
}
return 'fs-'.$hash.'@'.preg_replace("/.*@/", '', $email_address);
}
/**
* Fetch IMAP message by Message-ID.
*/
public static function fetchMessage($mailbox, $message_id)
{
$no_charset = false;
if (!$message_id) {
return null;
}
try {
$client = \MailHelper::getMailboxClient($mailbox);
$client->connect();
} catch (\Exception $e) {
\Helper::logException($e, '('.$mailbox->name.') Could not fetch specific message by Message-ID via IMAP:');
return null;
}
$imap_folders = $mailbox->getInImapFolders();
foreach ($imap_folders as $folder_name) {
try {
$folder = $client->getFolder($folder_name);
// Message-ID: <123@123.com>
$query = $folder->query()->text('<'.$message_id.'>')->leaveUnread()->limit(1);
if ($no_charset) {
$query->setCharset(null);
}
$messages = $query->get();
$last_error = '';
if (method_exists($client, 'getLastError')) {
$last_error = $client->getLastError();
}
if ($last_error && stristr($last_error, 'The specified charset is not supported')) {
// Solution for MS mailboxes.
// https://github.com/freescout-helpdesk/freescout/issues/176
$messages = $folder->query()->text('<'.$message_id.'>')->leaveUnread()->limit(1)->setCharset(null)->get();
$no_charset = true;
}
if (count($messages)) {
return $messages->first();
}
} catch (\Exception $e) {
\Helper::logException($e, '('.$mailbox->name.') Could not fetch specific message by Message-ID via IMAP:');
}
}
return null;
}
public static function oauthGetAuthorizationUrl($provider_code, $params)
{
$args = [];
switch ($provider_code) {
case self::OAUTH_PROVIDER_MICROSOFT:
// https://docs.microsoft.com/en-us/exchange/client-developer/legacy-protocols/how-to-authenticate-an-imap-pop-smtp-application-by-using-oauth
$args = [
'scope' => 'offline_access https://outlook.office.com/IMAP.AccessAsUser.All https://outlook.office.com/SMTP.Send',
'response_type' => 'code',
'approval_prompt' => 'auto',
'redirect_uri' => route('mailboxes.oauth_callback'),
];
$args = array_merge($args, $params);
$url = 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize?'.http_build_query($args);
break;
}
return $url;
}
public static function oauthGetAccessToken($provider_code, $params)
{
$token_data = [];
$post_params = [];
switch ($provider_code) {
case self::OAUTH_PROVIDER_MICROSOFT:
$post_params = [
'scope' => 'offline_access https://outlook.office.com/IMAP.AccessAsUser.All https://outlook.office.com/SMTP.Send',
"grant_type" => "authorization_code",
'redirect_uri' => route('mailboxes.oauth_callback'),
];
$post_params = array_merge($post_params, $params);
// Refreshing Access Token.
if (!empty($post_params['refresh_token'])) {
$post_params['grant_type'] = 'refresh_token';
}
// $postUrl = "/common/oauth2/token";
// $hostname = "login.microsoftonline.com";
$full_url = "https://login.microsoftonline.com/common/oauth2/v2.0/token";
// $headers = array(
// // "POST " . $postUrl . " HTTP/1.1",
// // "Host: login.windows.net",
// "Content-type: application/x-www-form-urlencoded",
// );
$curl = curl_init($full_url);
curl_setopt($curl, CURLOPT_POST, true);
//curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
curl_setopt($curl, CURLOPT_POSTFIELDS, $post_params);
curl_setopt($curl, CURLOPT_HTTPHEADER, array("application/x-www-form-urlencoded"));
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, true);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
$response = curl_exec($curl);
if ($response) {
$result = json_decode($response, true);
// [token_type] => Bearer
// [scope] => IMAP.AccessAsUser.All offline_access SMTP.Send User.Read
// [expires_in] => 4514
// [ext_expires_in] => 4514
// [expires_on] => 1646122657
// [not_before] => 1646117842
// [resource] => 00000002-0000-0000-c000-000000000000
// [access_token] => dd
// [refresh_token] => dd
// [id_token] => dd
if (!empty($result['access_token'])) {
$token_data['provider'] = self::OAUTH_PROVIDER_MICROSOFT;
$token_data['a_token'] = $result['access_token'];
$token_data['r_token'] = $result['refresh_token'];
//$token_data['id_token'] = $result['id_token'];
$token_data['issued_on'] = now()->toDateTimeString();
$token_data['expires_in'] = $result['expires_in'];
} elseif ($response) {
$token_data['error'] = $response;
} else {
$token_data['error'] = 'Response code: '.curl_getinfo($curl, CURLINFO_HTTP_CODE);
}
}
curl_close($curl);
break;
}
return $token_data;
}
public static function oauthDisconnect($provider_code, $redirect_uri)
{
switch ($provider_code) {
case self::OAUTH_PROVIDER_MICROSOFT:
return redirect()->away('https://login.microsoftonline.com/common/oauth2/v2.0/logout?post_logout_redirect_uri='.urlencode($redirect_uri));
break;
}
}
// public static function oauthGetProvider($provider_code, $params)
// {
// $provider = null;
// switch ($provider_code) {
// case self::OAUTH_PROVIDER_MICROSOFT:
// $provider = new \Stevenmaguire\OAuth2\Client\Provider\Microsoft([
// // Required
// 'clientId' => $params['client_id'],
// 'clientSecret' => $params['client_secret'],
// 'redirectUri' => route('mailboxes.oauth_callback'),
// //https://login.microsoftonline.com/common/oauth2/authorize';
// 'urlAuthorize' => 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize',
// 'urlAccessToken' => 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
// 'urlResourceOwnerDetails' => 'https://outlook.office.com/api/v1.0/me'
// ]);
// break;
// }
// return $provider;
// }
}

View File

@@ -2,47 +2,75 @@
namespace App\Misc;
use Zttp\Zttp;
class WpApi
{
const ENDPOINT_MODULES = 'freescout/v1/modules';
const METHOD_GET = 'get';
const METHOD_POST = 'post';
const METHOD_GET = 'GET';
const METHOD_POST = 'POST';
const ACTION_CHECK_LICENSE = 'check_license';
const ACTION_CHECK_LICENSE = 'check_license';
const ACTION_CHECK_LICENSES = 'check_licenses';
const ACTION_ACTIVATE_LICENSE = 'activate_license';
const ACTION_GET_VERSION = 'get_version';
const ACTION_DEACTIVATE_LICENSE = 'deactivate_license';
const ACTION_GET_VERSION = 'get_version';
public static $lastError;
public static function url($path)
public static function url($path, $alternative = false)
{
return \Config::get('app.freescout_api').$path;
if ($alternative) {
return \Config::get('app.freescout_alt_api').$path;
} else {
return \Config::get('app.freescout_api').$path;
}
}
public static function httpRequest($method, $url, $params)
{
$client = new \GuzzleHttp\Client();
if ($method == self::METHOD_POST) {
return $client->request('POST', $url, [
'connect_timeout' => 10,
'form_params' => $params,
]);
} else {
return $client->request('GET', $url, [
'connect_timeout' => 10,
'query' => $params,
]);
}
}
/**
* API request.
*/
public static function request($method, $endpoint, $params = [])
public static function request($method, $endpoint, $params = [], $alternative_api = false)
{
self::$lastError = null;
try {
$response = Zttp::$method(self::url($endpoint), $params);
$response = self::httpRequest($method, self::url($endpoint, $alternative_api), $params);
} catch (\Exception $e) {
self::$lastError = array(
'code' => $e->getCode(),
if (!$alternative_api) {
return self::request($method, $endpoint, $params, true);
}
\Helper::logException($e, 'WpApi');
self::$lastError = [
'code' => $e->getCode(),
'message' => $e->getMessage(),
);
];
return [];
}
// https://guzzle3.readthedocs.io/http-client/response.html
if ($response->status() < 500) {
$json = $response->json();
if (!empty($json['code']) && !empty($json['message']) &&
if ($response->getStatusCode() < 500) {
$json = \Helper::jsonToArray($response->getBody());
if (!empty($json['code']) && !empty($json['message']) &&
!empty($json['data']) && !empty($json['data']['status']) && $json['data']['status'] != 200
) {
self::$lastError = $json;
@@ -80,6 +108,18 @@ class WpApi
return self::request(self::METHOD_POST, $endpoint, $params);
}
/**
* Check module license.
*/
public static function checkLicenses($params)
{
$params['action'] = self::ACTION_CHECK_LICENSES;
$endpoint = self::ENDPOINT_MODULES;
return self::request(self::METHOD_POST, $endpoint, $params);
}
/**
* Activate module license.
*/
@@ -96,6 +136,22 @@ class WpApi
return self::request(self::METHOD_POST, $endpoint, $params);
}
/**
* Deactivate module license.
*/
public static function deactivateLicense($params)
{
$params['action'] = self::ACTION_DEACTIVATE_LICENSE;
$endpoint = self::ENDPOINT_MODULES;
if (!empty($params['module_alias'])) {
$endpoint .= '/'.$params['module_alias'];
}
return self::request(self::METHOD_POST, $endpoint, $params);
}
/**
* Get license details.
*/

View File

@@ -3,6 +3,7 @@
* 'active' parameter in module.json is not taken in account.
* Module 'active' flag is taken from DB.
*/
namespace App;
use Illuminate\Database\Eloquent\Model;
@@ -20,15 +21,16 @@ class Module extends Model
public static function getCached()
{
if (!self::$modules) {
if (!self::$modules) {
// At this stage modules table may not exist
try {
self::$modules = Module::all();
self::$modules = self::all();
} catch (\Exception $e) {
// Do nothing
}
}
return self::$modules;
}
return self::$modules;
}
public static function isActive($alias)
@@ -48,6 +50,7 @@ class Module extends Model
if ($save) {
$module->save();
}
return true;
}
@@ -76,9 +79,11 @@ class Module extends Model
/**
* Activate module license.
* @param [type] $alias [description]
* @param [type] $details_url [description]
* @return boolean [description]
*
* @param [type] $alias [description]
* @param [type] $details_url [description]
*
* @return bool [description]
*/
public static function activateLicense($alias, $license)
{
@@ -100,9 +105,10 @@ class Module extends Model
{
$module = self::getByAlias($alias);
if (!$module) {
$module = new Module();
$module = new self();
$module->alias = $alias;
}
return $module;
}
@@ -119,6 +125,13 @@ class Module extends Model
}
}
public static function setLicense($alias, $license)
{
$module = self::getByAliasOrCreate($alias);
$module->license = $license;
$module->save();
}
public static function normalizeAlias($alias)
{
return trim(strtolower($alias));
@@ -130,17 +143,94 @@ class Module extends Model
if ($modules) {
return self::getCached()->where('alias', $alias)->first();
} else {
return null;
return;
}
}
/**
* Deactivate module and update modules cache.
*/
public static function deactiveModule($alias)
public static function deactiveModule($alias, $clear_app_cache = true)
{
self::setActive($alias, false);
// Update modules cache
\Module::clearCache();
if ($clear_app_cache) {
\Artisan::call('freescout:clear-cache');
}
}
/**
* Get URL used to active and check license.
*
* @return [type] [description]
*/
public static function getAppUrl()
{
return parse_url(\Config::get('app.url'), PHP_URL_HOST);
}
/**
* Check missing extensions among required by module.
*
* @param [type] $required_extensions [description]
*
* @return [type] [description]
*/
public static function getMissingExtensions($required_extensions)
{
$missing = [];
$list = explode(',', $required_extensions ?? '');
if (!is_array($list) || !count($list)) {
return [];
}
foreach ($list as $ext) {
$ext = trim($ext);
if ($ext && !extension_loaded($ext)) {
$missing[] = $ext;
}
}
return $missing;
}
/**
* Check missing modules required by the module.
*/
public static function getMissingModules($required_modules, $modules = [])
{
$missing = [];
if (!$modules) {
$modules = \Module::all();
}
if (!is_array($required_modules) || !count($required_modules)) {
return [];
}
foreach ($required_modules as $alias => $version) {
$module = null;
foreach ($modules as $module_item) {
if ($module_item->alias == $alias) {
$module = $module_item;
}
}
if (!$module) {
$missing[$alias] = $version;
continue;
}
if (!self::isActive($alias) || !version_compare($module->version, $version, '>=')) {
$missing[$alias] = $version;
}
}
return $missing;
}
public static function formatName($name)
{
return preg_replace("/ Module$/", '', $name);
}
}

View File

@@ -9,12 +9,15 @@ namespace App\Notifications;
use App\Conversation;
use App\Subscription;
use App\Thread;
use App\Channels\RealtimeBroadcastChannel;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\BroadcastMessage;
use Illuminate\Notifications\Notification;
class BroadcastNotification extends Notification
class BroadcastNotification extends Notification implements ShouldQueue
{
use Queueable;
public $conversation;
public $thread;
public $mediums;
@@ -34,7 +37,8 @@ class BroadcastNotification extends Notification
/**
* Get the notification's delivery channels.
*
* @param mixed $user
* @param mixed $user
*
* @return array
*/
public function via($user)
@@ -47,7 +51,8 @@ class BroadcastNotification extends Notification
/**
* Get the broadcastable representation of the notification.
*
* @param mixed $notifiable
* @param mixed $notifiable
*
* @return BroadcastMessage
*/
public function toBroadcast($user)
@@ -67,6 +72,9 @@ class BroadcastNotification extends Notification
return $data;
}
// Try to convert to array.
$mediums = (array)$payload->mediums;
$thread = Thread::find($payload->thread_id);
if (empty($thread)) {
@@ -77,8 +85,7 @@ class BroadcastNotification extends Notification
$db_notification = new \Illuminate\Notifications\DatabaseNotification();
// HTML for the menu notification (uses same medium as for email)
if (in_array(Subscription::MEDIUM_EMAIL, $payload->mediums)) {
if (in_array(Subscription::MEDIUM_EMAIL, $mediums)) {
$web_notifications_info = [];
// Get last reply or note of the conversation to display it's text
@@ -110,7 +117,7 @@ class BroadcastNotification extends Notification
}
// Text and url for the browser push notification
if (in_array(Subscription::MEDIUM_BROWSER, $payload->mediums)) {
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]);
}

View File

@@ -1,15 +1,20 @@
<?php
/**
* Website notification (DB notification).
* Website notification (DB notification) to display notifications in the menu.
*/
namespace App\Notifications;
use App\Conversation;
use App\Thread;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Notification;
class WebsiteNotification extends Notification
class WebsiteNotification extends Notification implements ShouldQueue
{
use Queueable;
public $conversation;
public $thread;
@@ -28,7 +33,8 @@ class WebsiteNotification extends Notification
/**
* Get the notification's delivery channels.
*
* @param mixed $user
* @param mixed $user
*
* @return array
*/
public function via($user)
@@ -39,13 +45,14 @@ class WebsiteNotification extends Notification
/**
* Get the array representation of the notification.
*
* @param mixed $user
* @param mixed $user
*
* @return array
*/
public function toArray($user)
{
return [
'thread_id' => $this->thread->id,
'thread_id' => $this->thread->id,
'conversation_id' => $this->conversation->id,
];
}
@@ -79,22 +86,21 @@ class WebsiteNotification extends Notification
}
// Get last reply or note of the conversation to display it's text
if ($threads) {
$last_threads = Thread::whereIn('conversation_id', $threads->pluck('conversation_id')->unique()->toArray())
// Select must contain all fields from orderBy() to avoid:
// General error: 3065 Expression #1 of ORDER BY clause is not in SELECT
->select(['id', 'conversation_id', 'body', 'created_at'])
->whereIn('type', [Thread::TYPE_CUSTOMER, Thread::TYPE_MESSAGE, Thread::TYPE_NOTE])
->distinct('conversation_id')
->orderBy('created_at')
// We can not use groupBy because of "isn't in GROUP BY"
//->groupBy('conversation_id')
->get();
}
// if ($threads) {
// $last_threads = Thread::whereIn('conversation_id', $threads->pluck('conversation_id')->unique()->toArray())
// // Select must contain all fields from orderBy() to avoid:
// // General error: 3065 Expression #1 of ORDER BY clause is not in SELECT
// ->select(['id', 'conversation_id', 'body', 'created_at'])
// ->whereIn('type', [Thread::TYPE_CUSTOMER, Thread::TYPE_MESSAGE, Thread::TYPE_NOTE])
// ->distinct('conversation_id')
// ->orderBy('created_at', 'desc')
// // We can not use groupBy because of "isn't in GROUP BY"
// //->groupBy('conversation_id')
// ->get();
// }
// Populate all collected data into array
foreach ($notifications as $notification) {
$conversation_number = '';
if (!empty($notification->data['number'])) {
$conversation_number = $notification->data['number'];
@@ -123,14 +129,14 @@ class WebsiteNotification extends Notification
continue;
}
$last_thread_body = '';
$conversation = null;
// $last_thread_body = '';
// $conversation = null;
$last_thread = $last_threads->firstWhere('conversation_id', $thread->conversation_id);
if ($last_thread) {
$last_thread_body = $last_thread->body;
$conversation = $last_thread->conversation;
}
// $last_thread = $last_threads->firstWhere('conversation_id', $thread->conversation_id);
// if ($last_thread) {
$last_thread_body = $thread->body;
$conversation = $thread->conversation;
//}
if (empty($conversation)) {
continue;
}

View File

@@ -0,0 +1,18 @@
<?php
namespace App\Observers;
use App\Email;
class EmailObserver
{
/**
* Email created.
*
* @param User $user
*/
public function created(Email $email)
{
\Eventy::action('email.created', $email);
}
}

View File

@@ -36,5 +36,7 @@ class MailboxObserver
$mailbox->users()->delete();
$mailbox->conversations()->delete();
$mailbox->folders()->delete();
\Eventy::action('mailbox.before_delete', $mailbox);
}
}

View File

@@ -12,7 +12,7 @@ class SendLogObserver
public function created(SendLog $send_log)
{
// Update status for thread if any
if ($send_log->thread_id && ($send_log->customer_id || ($send_log->user_id && $send_log->user_id == $send_log->thread->user_id)) ) {
if ($send_log->thread_id && ($send_log->customer_id || ($send_log->user_id && $send_log->user_id == $send_log->thread->user_id))) {
$send_log->thread->send_status = $send_log->status;
$send_log->thread->save();
}

View File

@@ -24,7 +24,11 @@ class ThreadObserver
if (!in_array($thread->type, [Thread::TYPE_CUSTOMER])) {
$conversation->user_updated_at = $now;
}
if (in_array($thread->type, [Thread::TYPE_CUSTOMER, Thread::TYPE_MESSAGE]) && $thread->state == Thread::STATE_PUBLISHED) {
if ((in_array($thread->type, [Thread::TYPE_CUSTOMER, Thread::TYPE_MESSAGE])
|| ($conversation->isPhone() && in_array($thread->type, [Thread::TYPE_NOTE])))
&& $thread->state == Thread::STATE_PUBLISHED
) {
// $conversation->cc = $thread->cc;
// $conversation->bcc = $thread->bcc;
$conversation->last_reply_at = $now;
@@ -33,6 +37,36 @@ class ThreadObserver
if ($conversation->source_via == Conversation::PERSON_CUSTOMER) {
$conversation->read_by_user = false;
}
// Update preview.
if (in_array($thread->type, [Thread::TYPE_CUSTOMER, Thread::TYPE_MESSAGE, Thread::TYPE_NOTE])
&& $thread->state == Thread::STATE_PUBLISHED
&& !$thread->isForward()
&& ($conversation->threads_count > 1 || $thread->type == Thread::TYPE_NOTE)
) {
$conversation->setPreview($thread->body);
}
$conversation->save();
// $is_new_conversation = false;
// if ($conversation->threads_count == 0
// && in_array($thread->type, [Thread::TYPE_CUSTOMER, Thread::TYPE_MESSAGE, Thread::TYPE_NOTE])
// && $thread->state == Thread::STATE_PUBLISHED
// ) {
// $is_new_conversation = true;
// }
// User threads are created as drafts first.
// if ($thread->state == Thread::STATE_PUBLISHED) {
// \Eventy::action('thread.created', $thread);
// }
// Real time for user notifications is sent using events.
if ($thread->type == Thread::TYPE_CUSTOMER
|| ($thread->type == Thread::TYPE_MESSAGE && $thread->state == Thread::STATE_DRAFT)
) {
Conversation::refreshConversations($conversation, $thread);
}
}
}

View File

@@ -1,13 +1,17 @@
<?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;
use Illuminate\Database\Eloquent\Model;
class Option extends Model
{
// Value returned when cache contains default value.
const CACHE_DEFAULT_VALUE = 'CACHE_DEFAULT_VALUE';
// todo: cache like in WordPress (fetch all options from DB each time)
public static $cache = [];
@@ -23,9 +27,10 @@ class Option extends Model
/**
* Set an option.
*
* @param string $name
* @param string $value
* @return boolean
* @param string $name
* @param string $value
*
* @return bool
*/
public static function set($name, $value)
{
@@ -45,7 +50,7 @@ class Option extends Model
$serialized_value = self::maybeSerialize($value);
$option = Option::firstOrCreate(
$option = self::firstOrCreate(
['name' => $name], ['value' => $serialized_value]
);
@@ -62,60 +67,148 @@ class Option extends Model
/**
* Get option.
*
* @param string $name
* @param string $name
*
* @return string
*/
public static function get($name, $default = false, $decode = true)
{
// If not passed, get default value from config
// If not passed, get default value from config
if (func_num_args() == 1) {
$default = self::getDefault($name, $default);
}
if (isset(self::$cache[$name])) {
return self::$cache[$name];
if (self::$cache[$name] == self::CACHE_DEFAULT_VALUE) {
return $default;
} else {
return self::$cache[$name];
}
}
$option = Option::where('name', (string)$name)->first();
$option = self::where('name', (string) $name)->first();
if ($option) {
if ($decode) {
$value = self::maybeUnserialize($option->value);
} else {
$value = $option->value;
}
self::$cache[$name] = $value;
} else {
$value = $default;
self::$cache[$name] = self::CACHE_DEFAULT_VALUE;
}
self::$cache[$name] = $value;
return $value;
}
public static function getDefault($option_name, $default)
public static function getDefault($option_name, $default = false)
{
$options = \Config::get('app.options');
if (isset($options[$option_name]) && isset($options[$option_name]['default'])) {
return $options[$option_name]['default'];
} else {
// Try to get default option value from module's config.
preg_match("/^([a-z_]+)\.(.*)/", $option_name, $m);
if (!empty($m[1]) && !empty($m[2])) {
$module_alias = $m[1];
$modle_option_name = $m[2];
$module_options = \Config::get($module_alias.'.options');
if (isset($module_options[$modle_option_name]) && isset($module_options[$modle_option_name]['default'])) {
return $module_options[$modle_option_name]['default'];
}
}
return $default;
}
}
public static function isDefaultSet($option_name)
{
$options = \Config::get('app.options');
return isset($options[$option_name]) && isset($options[$option_name]['default']);
}
/**
* Get multiple options.
*
* @param [type] $name [description]
* @param bool $default [description]
* @param bool $decode [description]
*
* @return [type] [description]
*/
public static function getOptions($options, $defaults = [], $decode = [])
{
$values = [];
// Check in cache first
// Return if we can get all options from cache
foreach ($options as $name) {
if (isset(self::$cache[$name])) {
if (self::$cache[$name] == self::CACHE_DEFAULT_VALUE) {
if (!isset($defaults[$name])) {
$default = self::getDefault($name);
} else {
$default = $defaults[$name];
}
$values[$name] = $default;
} else {
$values[$name] = self::$cache[$name];
}
}
}
if (count($values) == count($options)) {
return $values;
} else {
$values = [];
}
$db_options = self::whereIn('name', $options)->get();
foreach ($options as $name) {
// If not passed, get default value from config
if (!isset($defaults[$name])) {
$default = self::getDefault($name);
} else {
$default = $defaults[$name];
}
$db_option = $db_options->where('name', $name)->first();
if ($db_option) {
// todo: decode
if (1 || $decode) {
$value = self::maybeUnserialize($db_option->value);
} else {
$value = $db_option->value;
}
self::$cache[$name] = $value;
} else {
$value = $default;
self::$cache[$name] = self::CACHE_DEFAULT_VALUE;
}
$values[$name] = $value;
}
return $values;
}
public static function remove($name)
{
Option::where('name', (string)$name)->delete();
self::where('name', (string) $name)->delete();
}
/**
* Serialize data, if needed.
*/
public static function maybeSerialize($data) {
public static function maybeSerialize($data)
{
if (is_array($data) || is_object($data)) {
return serialize( $data );
return serialize($data);
}
return $data;
}
@@ -130,8 +223,10 @@ class Option extends Model
} catch (\Exception $e) {
// Do nothing
}
return $original;
}
return $original;
}
@@ -139,16 +234,17 @@ class Option extends Model
* Check value to find if it was serialized.
* Serialized data is always a string.
*/
public static function isSerialized($data, $strict = true) {
public static function isSerialized($data, $strict = true)
{
// if it isn't a string, it isn't serialized.
if (!is_string($data )) {
if (!is_string($data)) {
return false;
}
$data = trim($data);
if ('N;' == $data) {
return true;
}
if (strlen( $data ) < 4) {
if (strlen($data) < 4) {
return false;
}
if (':' !== $data[1]) {
@@ -161,36 +257,41 @@ class Option extends Model
}
} else {
$semicolon = strpos($data, ';');
$brace = strpos($data, '}');
$brace = strpos($data, '}');
// Either ; or } must exist.
if (false === $semicolon && false === $brace)
if (false === $semicolon && false === $brace) {
return false;
}
// But neither must be in the first X characters.
if (false !== $semicolon && $semicolon < 3)
if (false !== $semicolon && $semicolon < 3) {
return false;
if (false !== $brace && $brace < 4)
}
if (false !== $brace && $brace < 4) {
return false;
}
}
$token = $data[0];
switch ($token) {
case 's' :
case 's':
if ($strict) {
if ('"' !== substr( $data, -2, 1)) {
if ('"' !== substr($data, -2, 1)) {
return false;
}
} elseif (false === strpos( $data, '"')) {
} elseif (false === strpos($data, '"')) {
return false;
}
// or else fall through
case 'a' :
case 'O' :
return (bool)preg_match("/^{$token}:[0-9]+:/s", $data);
case 'b' :
case 'i' :
case 'd' :
case 'a':
case 'O':
return (bool) preg_match("/^{$token}:[0-9]+:/s", $data);
case 'b':
case 'i':
case 'd':
$end = $strict ? '$' : '';
return (bool)preg_match("/^{$token}:[0-9.E-]+;$end/", $data);
return (bool) preg_match("/^{$token}:[0-9.E-]+;$end/", $data);
}
return false;
}

View File

@@ -3,6 +3,7 @@
namespace App\Policies;
use App\Conversation;
use App\Mailbox;
use App\User;
use Illuminate\Auth\Access\HandlesAuthorization;
@@ -31,6 +32,26 @@ class ConversationPolicy
}
}
/**
* Cached version.
*
* @param User $user [description]
* @param Conversation $conversation [description]
* @return [type] [description]
*/
public function viewCached(User $user, Conversation $conversation)
{
if ($user->isAdmin()) {
return true;
} else {
if ($conversation->mailbox->users_cached->contains($user)) {
return true;
} else {
return false;
}
}
}
/**
* Determine whether the user can update the conversation.
*
@@ -63,4 +84,21 @@ class ConversationPolicy
return $user->hasPermission(User::PERM_DELETE_CONVERSATIONS);
}
}
/**
* Determine whether current user can move conversations
*
* @param \App\User $user
* @param \App\Mailbox $mailbox
*
* @return mixed
*/
public function move(User $user)
{
// First check this, because it is cached in conversation page
if (count($user->mailboxesCanView(true)) > 1) {
return true;
}
return Mailbox::count() > 1;
}
}

View File

@@ -23,7 +23,7 @@ class FolderPolicy
if ($user->isAdmin()) {
return true;
} else {
if ($folder->user_id == $user->id || $user->mailboxes->contains($folder->mailbox)) {
if ($folder->user_id == $user->id || $user->mailboxesSettings()->pluck('mailbox_id')->contains($folder->mailbox_id)) {
return true;
} else {
return false;

View File

@@ -46,6 +46,40 @@ class MailboxPolicy
}
}
/**
* Determine whether the user can view mailbox conversations.
*
* @param \App\User $user
*
* @return mixed
*/
public function viewCached(User $user, Mailbox $mailbox)
{
if ($user->isAdmin()) {
return true;
} else {
// Use cached users for Realtime events
if ($mailbox->users_cached->contains($user)) {
return true;
} else {
return false;
}
}
}
/**
* Determine whether the user can admin the mailbox.
*
* @param \App\User $user
* @param \App\Mailbox $mailbox
*
* @return mixed
*/
public function admin(User $user, Mailbox $mailbox)
{
return $user->isAdmin();
}
/**
* Determine whether the user can update the mailbox.
*
@@ -56,7 +90,75 @@ class MailboxPolicy
*/
public function update(User $user, Mailbox $mailbox)
{
if ($user->isAdmin()) {
if ($user->isAdmin() || $user->canManageMailbox($mailbox->id)) {
return true;
} else {
return false;
}
}
/**
* Determine whether the user can update the mailbox auto reply.
*
* @param \App\User $user
* @param \App\Mailbox $mailbox
*
* @return mixed
*/
public function updateAutoReply(User $user, Mailbox $mailbox)
{
if ($user->isAdmin() || $user->hasManageMailboxPermission($mailbox->id, 'auto')) {
return true;
} else {
return false;
}
}
/**
* Determine whether the user can update the mailbox Permissions.
*
* @param \App\User $user
* @param \App\Mailbox $mailbox
*
* @return mixed
*/
public function updatePermissions(User $user, Mailbox $mailbox)
{
if ($user->isAdmin() || $user->hasManageMailboxPermission($mailbox->id, 'perm')) {
return true;
} else {
return false;
}
}
/**
* Determine whether the user can update the mailbox Permissions.
*
* @param \App\User $user
* @param \App\Mailbox $mailbox
*
* @return mixed
*/
public function updateSettings(User $user, Mailbox $mailbox)
{
if ($user->isAdmin() || $user->hasManageMailboxPermission($mailbox->id, 'edit')) {
return true;
} else {
return false;
}
}
/**
* Determine whether the user can update the mailbox Email Signature.
*
* @param \App\User $user
* @param \App\Mailbox $mailbox
*
* @return mixed
*/
public function updateEmailSignature(User $user, Mailbox $mailbox)
{
if ($user->isAdmin() || $user->hasManageMailboxPermission($mailbox->id, 'sig')) {
return true;
} else {
return false;

View File

@@ -0,0 +1,33 @@
<?php
namespace App\Policies;
use App\Thread;
use App\User;
use Illuminate\Auth\Access\HandlesAuthorization;
class ThreadPolicy
{
use HandlesAuthorization;
/**
* Determine whether the user can edit the thread.
*
* @param \App\User $user
* @param \App\Thread $thread
*
* @return mixed
*/
public function edit(User $user, Thread $thread)
{
if (($thread->created_by_user_id
&& in_array($thread->type, [Thread::TYPE_MESSAGE, Thread::TYPE_NOTE])
&& ($user->isAdmin() || ($user->hasPermission(User::PERM_EDIT_CONVERSATIONS) && $thread->created_by_user_id == $user->id)))
|| ($thread->created_by_customer_id && in_array($thread->type, [Thread::TYPE_CUSTOMER]))
) {
return true;
} else {
return false;
}
}
}

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