Compare commits

...

1105 Commits

Author SHA1 Message Date
FreeScout
fe4c69e3a9 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2022-06-20 00:13:22 -07:00
FreeScout
d961797c40 v1.8.19 2022-06-20 00:12:17 -07:00
FreeScout
b8dd8d30a6 Allow to log in in the mobile app when the root is redirecting - closes #2039 2022-06-19 23:55:22 -07:00
FreeScout
213fb80f95 Preservce code blocks when editing threads 2022-06-19 06:00:58 -07:00
FreeScout
7531eb4d64 Update Czech translation 2022-06-18 03:23:23 -07:00
FreeScout
f7c9d08b6c Pass conversation to conversation.is_in_folder_allowed hook 2022-06-17 22:29:31 -07:00
FreeScout
31995613ba mailbox.incoming_settings_before_save hook 2022-06-17 10:49:40 -07:00
FreeScout
529df15375 fetch_emails.after_set_seen hook 2022-06-17 10:48:59 -07:00
FreeScout
abfbf9959a Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2022-06-14 23:43:31 -07:00
FreeScout
9caa1e10cc v1.8.18 2022-06-14 23:43:03 -07:00
FreeScout
81139ab8b2 Adjust customer form and snippet 2022-06-14 23:41:56 -07:00
FreeScout
5f0510484e Enhancement of customer form #2034 from nykopol/master
Enhancement of customer form
2022-06-15 09:24:03 +03:00
FreeScout
c87b3a97d7 Do not send notifications to deleted users 2022-06-14 00:18:50 -07:00
FreeScout
74712cf2d9 search.assignees filter 2022-06-14 00:18:35 -07:00
FreeScout
40e0776219 Update subscription hooks 2022-06-10 02:39:19 -07:00
FreeScout
dc1d3ec5da Add EMAIL_DELETED_SUFFIX constatnt to User 2022-06-09 23:54:29 -07:00
FreeScout
d72bc81563 mailbox.permissions.before_access_settings hook 2022-06-09 05:13:41 -07:00
FreeScout
4d3bddc4d2 Folder meta functions 2022-06-09 05:13:23 -07:00
FreeScout
3cdbeaa768 Subscription hooks 2022-06-09 01:41:30 -07:00
FreeScout
13a9ca2ff3 User mailbox access hooks 2022-06-09 00:00:08 -07:00
Johan Desmyter
53f6c18cbe edit and show the existing phone type information on customer 2022-06-08 15:08:49 +03:00
Johan Desmyter
3211037aba add basic html validation on customer form 2022-06-08 13:59:26 +03:00
FreeScout
f5bf5afe14 Add vendor files to dist 2022-06-06 22:44:49 -07:00
FreeScout
4f65bbc723 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2022-06-06 22:43:19 -07:00
FreeScout
56e8e3862b v1.8.17 2022-06-06 22:42:54 -07:00
FreeScout
0d53e23151 Allow acess to /.well-known on Apache 2022-06-06 22:25:50 -07:00
FreeScout
dcdc372e45 Fix Passing null to parameter in User - closes #2020 2022-06-05 03:21:31 -07:00
FreeScout
ca847752df Third-party modules section in Modules 2022-06-03 11:04:50 -07:00
FreeScout
6156dc0824 Fix creating phone conversation on PHP 8.1 - closes #2028 2022-06-03 03:06:30 -07:00
FreeScout
04374dccdf Authentication hooks 2022-06-03 01:49:26 -07:00
FreeScout
1e4e67cc37 Hooks for URL in user email notifications 2022-06-03 01:47:47 -07:00
FreeScout
338df4aa44 Add Korean language 2022-06-01 04:51:09 -07:00
FreeScout
fc24702844 Implement IMAP Folder To Save Outgoing Replies for oAuth IMAP connection - closes #2018 2022-06-01 04:36:51 -07:00
FreeScout
fb82f16ef6 Merge pull request #2022 from e1mo/e1mo/app-key-file
Add APP_KEY_FILE environment variable
2022-06-01 12:53:52 +03:00
Moritz 'e1mo' Fromm
d915c646bb Add APP_KEY_FILE environment variable
In lots environments having files with secrets is common (e.g. with
NixOS where static secrets in config files are not secure). This new
environment variable adds support for this. If a APP_KEY is defined it
will take precedence ofer APP_KEY_FILE
Tests have been added to show this beaviour.
2022-05-31 17:33:52 +02:00
FreeScout
400ec40ff1 Added Czech language 2022-05-29 22:52:55 -07:00
FreeScout
d73f64b669 Update Polish language 2022-05-26 03:25:17 -07:00
FreeScout
403a160917 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2022-05-24 02:43:01 -07:00
FreeScout
3904e6d120 v1.8.16 2022-05-24 02:42:28 -07:00
FreeScout
bcdc2e793e Hooks for mailbox icons and customer default avatar #2012 from RienNeVaPlus/master
Hooks for mailbox icons and customer default avatar
2022-05-24 12:41:30 +03:00
FreeScout
cc68ee3d27 Avoid customer reply duplication loop - closes #2003 2022-05-24 02:38:41 -07:00
RienNeVaPlus
0b0ad5067c Fix argument of filter customer.default_avatar 2022-05-24 11:15:18 +02:00
RienNeVaPlus
b391d4496c Rename action to mailbox.update.before_name 2022-05-24 11:13:57 +02:00
FreeScout
31df77a0b9 Fix fetching issues 2022-05-24 01:30:19 -07:00
RienNeVaPlus
5e053d2954 Add action mailbox.before_name 2022-05-24 09:42:07 +02:00
RienNeVaPlus
3cfa02fde2 Add action mailbox.before_name 2022-05-24 09:40:51 +02:00
RienNeVaPlus
e3a5ac1432 Add action mailbox.before_name 2022-05-24 09:39:57 +02:00
RienNeVaPlus
97abf1da76 Add action mailbox.before_name 2022-05-24 09:39:03 +02:00
RienNeVaPlus
1a28d142f2 Add action mailbox.before_name 2022-05-24 09:38:01 +02:00
RienNeVaPlus
6dcd0dc8cc Allow file uploads in mailbox update form 2022-05-24 09:36:35 +02:00
RienNeVaPlus
23d812cdd4 Add action mailbox.update_before_name 2022-05-24 09:35:59 +02:00
RienNeVaPlus
90270ad31f Add filter customer.default_avatar 2022-05-24 09:34:03 +02:00
FreeScout
5ed8d90c70 Add [Third-Party] suffix to third-party modules 2022-05-16 23:22:06 -07:00
FreeScout
1f2182d014 Add [Third-Party] suffix to third-party modules 2022-05-16 22:11:08 -07:00
FreeScout
c1a2b819ec Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2022-05-16 21:44:37 -07:00
FreeScout
2b8e3fda63 v1.8.15 2022-05-16 21:44:09 -07:00
FreeScout
8bb0988010 Allow passwords with spaces during installation - closes #1998 2022-05-16 07:20:53 -07:00
FreeScout
5c4eaa17cf Fix Canada/Newfoundland timezone name - closes #1997 2022-05-16 07:01:41 -07:00
FreeScout
4bb44a8d1a Fix Call to a member function sortBy() on array on fetching emails - closes #1984 2022-05-16 06:55:21 -07:00
FreeScout
826418d5e4 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2022-05-13 00:08:55 -07:00
FreeScout
c2810be1b8 v1.8.14 2022-05-13 00:08:21 -07:00
FreeScout
3243d57b01 editor.vars hook 2022-05-12 22:57:13 -07:00
FreeScout
9bf1637320 User setData() method 2022-05-12 04:22:55 -07:00
FreeScout
ae415033dd Improve modules activation process 2022-05-11 02:19:07 -07:00
FreeScout
41d2743816 Fix Undefined property conversation in SendEmailReplyError job - closes #1974 2022-05-11 02:02:35 -07:00
FreeScout
2e6eb810cd Fix searching Phone conversations - closes #1986 2022-05-11 01:56:22 -07:00
FreeScout
7f99ef2c74 Fix PHP81 issue on deactivating a license 2022-05-10 04:39:23 -07:00
FreeScout
fda79460b8 Fix typo in update.sh #1977 from Pablohn26/patch-1
Fix typo in update.sh
2022-05-06 10:34:44 +03:00
Pablo Hinojosa
b91155c2ce Fix typo 2022-05-05 20:03:58 +02:00
FreeScout
83e2b0af7f Improve error message when user replies from random email address - #1971 2022-05-05 04:23:42 -07:00
FreeScout
d82b3dc896 Set SERVER HTTPS variable if needed in HttpsRedirect middleware 2022-05-03 22:47:27 -07:00
FreeScout
7f732bcfef Fix closed folder sorting - #1965 2022-05-03 03:55:29 -07:00
FreeScout
32a6c46e8a Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2022-05-01 04:51:25 -07:00
FreeScout
097615ef69 v1.8.13 2022-05-01 04:50:50 -07:00
FreeScout
ebaddd76a7 Allow to change Default Redirect from mailbox dropdown menu - closes #1942 2022-04-30 03:43:35 -07:00
FreeScout
aa4105e0cd Show error in the log when fetching from custom IMAP folder - closes #1961 2022-04-30 01:39:09 -07:00
FreeScout
487e0bf370 Update translations 2022-04-30 01:25:55 -07:00
FreeScout
8121c489d5 Update Gmail instruction link 2022-04-29 23:26:33 -07:00
FreeScout
3362bbeb45 Update general_help_request 2022-04-29 23:25:53 -07:00
FreeScout
08796da55e Make some modifications in RTL style to fix view issues #1945 from rhashnull/master
Make some modifications in RTL style to fix view issues.
2022-04-30 08:03:23 +03:00
Arash Khajelou
b68b17c3df Fix the issue with tranlating :number parameter. 2022-04-25 16:33:44 +04:30
Arash Khajelou
218605334c Make some mofigications in rtl style to fix view issues. 2022-04-25 16:30:35 +04:30
FreeScout
8d9f9bead8 Adjust permission name 2022-04-23 23:26:42 -07:00
FreeScout
86e1c292f2 Update French translation 2022-04-23 23:14:21 -07:00
FreeScout
b4c334ed9c Add customer.company variable 2022-04-23 23:07:37 -07:00
FreeScout
3b3c23451b Add customer.company variable to templating #1931 from e1mo/master
Add customer.company variable to templating
2022-04-24 09:03:22 +03:00
FreeScout
8f71f77b1b Merge branch 'master' of github.com:freescout-helpdesk/freescout 2022-04-23 23:01:12 -07:00
FreeScout
84f53fd682 Do not show separator in the plain text email body for the first thread in the loop - closes #1927 2022-04-23 23:00:08 -07:00
FreeScout
a3a27259fc Update readme 2022-04-22 21:56:53 -07:00
Moritz 'e1mo' Fromm
eff6c8c351 Add customer.company variable to templating
A corresponding Feature-Request exists here:
https://feedback.userreport.com/25a3cb5f-e4bd-4470-b6f3-79fcfaa8e90f
2022-04-21 10:36:44 +02:00
FreeScout
2d21c21256 Merge pull request #1923 from RienNeVaPlus/master
Implement actions & filters for an additional user field
2022-04-18 17:18:48 +03:00
RienNeVaPlus
cf01a524e5 Add filter user.setup_save 2022-04-18 01:40:53 +02:00
RienNeVaPlus
51e2f71afc Add filter user.create_save 2022-04-18 01:39:39 +02:00
RienNeVaPlus
358781905f Add action user.setup.before_job_title 2022-04-18 01:38:30 +02:00
RienNeVaPlus
6b782ede62 Add action user.edit.before_email 2022-04-18 01:37:20 +02:00
RienNeVaPlus
425080093b Add action user.create.before_email 2022-04-18 01:36:01 +02:00
FreeScout
415b20020a Add vendor files to dist 2022-04-14 06:13:12 -07:00
FreeScout
29b36472c9 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2022-04-14 06:10:41 -07:00
FreeScout
5f6bd2d993 v1.8.12 2022-04-14 06:09:42 -07:00
FreeScout
8fb6d9152f Trigger phone call when clicking on customer phone - closes #1899 2022-04-14 05:05:07 -07:00
FreeScout
1884048f9c Fix getFullName() on null after deleting a customer - closes #1909 2022-04-14 05:00:10 -07:00
FreeScout
1d859d3fb9 Fix resettings a password on PHP 8.1 - closes #1911 2022-04-14 04:42:48 -07:00
FreeScout
b41e78e71e Merge pull request #1912 from Lars-/master
Multiple filters
2022-04-14 14:25:34 +03:00
FreeScout
e8afbd85a9 Fix clearing Fetching logs - closes #1915 2022-04-14 04:21:54 -07:00
Lars Jansen
d9a8fc8e8b Actions before and after thread recipients 2022-04-12 16:07:12 +02:00
Lars Jansen
a15b65f819 Filter outgoing name 2022-04-12 16:05:16 +02:00
FreeScout
7dc35d143a menu.user.name_append hook 2022-04-06 07:08:00 -07:00
FreeScout
0bdb22e934 Take create_thread parameter into account in changeUser() method 2022-04-06 07:07:28 -07:00
FreeScout
09b777181a Hooks to adjust assignee list 2022-04-06 04:51:31 -07:00
FreeScout
18217c58b6 Hooks to allow adding fields to the user profile 2022-04-06 03:23:51 -07:00
FreeScout
147926bcf1 Add vendor files to dist 2022-04-06 00:03:46 -07:00
FreeScout
3c904fc048 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2022-04-06 00:00:26 -07:00
FreeScout
d1736d9f09 jobs.send_reply_to_customer.headers hook 2022-04-05 23:56:38 -07:00
FreeScout
8ef1fd811b Merge pull request #1882 from DavidAnderson684/patch-3
Add filter allowing customisation of outgoing headers
2022-04-06 09:53:58 +03:00
FreeScout
a64cfa6307 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2022-04-05 23:52:57 -07:00
FreeScout
3f80845371 v1.8.11 2022-04-05 23:52:25 -07:00
FreeScout
80382199ee Fix PHP 8.1 preg_match issue in Mail.php - closes #1888 2022-04-05 22:21:19 -07:00
FreeScout
292e9ec0ae Add noindex meta tag 2022-04-05 22:19:24 -07:00
FreeScout
59b57e268d Fix Passing null to parameter in Kernel.php - closes #1884 2022-04-05 07:25:26 -07:00
FreeScout
153e68f7ae Make symfony/http-foundation/Request.php PHP 8.1 compatible - closes #1841 2022-04-05 07:19:47 -07:00
FreeScout
0b3d36cee0 Allow to translate a message shows after replying - closes #1862 2022-04-05 07:04:38 -07:00
David Anderson
ce2557dd24 Add filter allowing customisation of outgoing headers 2022-04-05 13:52:49 +00:00
FreeScout
a8f5835a39 Merge pull request #1871 from Lars-/master
Add filters to be able to filter SMTP settings when sending a mail
2022-04-05 16:35:16 +03:00
FreeScout
9a273473d5 Fixed undefined variable error on muting notifications #1857 from Jobs-Foundation/master
Fixed undefined variable error on muting notifications
2022-04-05 16:28:23 +03:00
Lars Jansen
25b0c19db1 Add filters to be able to filter SMTP settings when sending a mail 2022-04-02 09:01:45 +02:00
Zachary Schroeder
bbc3e7f3aa fixed undefined variable error 2022-03-29 13:56:13 -05:00
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
1220 changed files with 67522 additions and 63847 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
@@ -29,4 +32,4 @@ DB_PASSWORD=
APP_KEY=
# Uncomment to see errors in your browser, don't forget to comment it back when debugging finished
#APP_DEBUG=true
#APP_DEBUG=true

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,13 @@
---
name: General Help Request
about: Create a general help request
---
* Have you read FAQ? https://github.com/freescout-helpdesk/freescout/wiki/FAQ
* If you have "Whoops something went wrong" error see: https://github.com/freescout-helpdesk/freescout/wiki/Installation-Guide#11-troubleshooting
* 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...

1
.gitignore vendored
View File

@@ -27,6 +27,7 @@ Thumbs.db
/Modules
/Modules/**/.git
/public/modules
/public/docs
/storage/.ignore_locales
/storage/.installed
/tools

View File

@@ -5,5 +5,6 @@
# 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.
RewriteCond %{REQUEST_URI} !/\.well\-known/?.*
RewriteRule (.*) public/$1 [L]
</IfModule>

View File

@@ -3,6 +3,8 @@ language: php
php:
- 7.0
- 7.2
- 7.4
- 8.0
before_script:
- cp .env.travis .env

View File

@@ -5,12 +5,11 @@
<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://freescout-helpdesk.github.io/img/badges/travis.svg)](https://freescout-helpdesk.github.io/img/badges/travis.svg) [![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 without giving up privacy or locking yourself into a service you don't control. FreeScout has been developed from scratch and is not using any copyrighted Help Scout or Zendesk materials.
**FreeScout** is the super lightweight 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)
@@ -18,29 +17,34 @@
* [Features](#features)
* [Demo](#demo)
* [Mobile Apps](#mobile-apps)
* [Modules](#modules)
* [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
* No limitations on the number of users, tickets, etc.
* No limitations on the number of users, tickets, mailboxes, etc.
* 100% Mobile-friendly.
* Multilingual (English, French, Italian, Portuguese, Russian, Dutch).
* Multilingual (English, Croatian, Czech, Danish, Dutch, French, German, Italian, Japanese, Korean, Persian, Polish, Portuguese, Russian, Spanish, Slovak, Swedish).
* Seamless email integration.
* Suppports modern Microsoft Exchange authentication.
* Web installer & updater.
* Starred conversations.
* Forwarding conversations.
* Merging conversations.
* Moving conversations between mailboxes.
* Phone conversations.
* Sending new conversations to multiple recipients at once.
* Collision detection – notice is shown when two agents open the same conversation.
* Push notifications.
* Following a conversation.
* Auto reply.
* Notes.
* Internal notes.
* Pasting screenshots from the clipboard into the reply area.
* Configuring notifications on a per user basis.
* Open tracking.
@@ -55,24 +59,18 @@ Need anything else? Suggest features [here](https://freescout.net/request-featur
## Mobile Apps
<a href="https://freescout.net/android-app/" target="_blank"><img alt="Android App" src="https://freescout-helpdesk.github.io/img/apps/android.png" width="200px" /></a>
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.
## Modules
[Available modules](https://freescout.net/modules/)
<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 - 7.3
* PHP 7.1 - 8.1
* MySQL 5.0+ / MariaDB 5.0+ / PostgreSQL
## 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)
## Installation
[Installation Guide](https://github.com/freescout-helpdesk/freescout/wiki/Installation-Guide)
@@ -82,13 +80,35 @@ 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
* [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/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

@@ -96,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));
}
}

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,19 +57,38 @@ 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 = preg_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();
@@ -84,6 +100,20 @@ class Attachment extends Model
$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,21 +123,20 @@ 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,
];
}
/**
@@ -149,13 +178,18 @@ class Attachment extends Model
*
* @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;
@@ -189,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);
}
/**
@@ -202,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)
@@ -260,7 +337,7 @@ class Attachment extends Model
{
// Delete from disk
foreach ($attachments as $attachment) {
Storage::delete($attachment->getStorageFilePath());
$attachment->getDisk()->delete($attachment->getStorageFilePath());
}
// Delete from DB
@@ -268,21 +345,65 @@ class Attachment extends Model
}
/**
* Generate dummy ID for attachment.
* Not used for now.
* Delete attachments and update Thread & Conversation.
*/
/*public static function genrateDummyId()
public static function deleteAttachments($attachments)
{
if (!$this->id) {
// Math.random should be unique because of its seeding algorithm.
// Convert it to base 36 (numbers + letters), and grab the first 9 characters
// after the decimal.
$hash = mt_rand() / mt_getrandmax();
$hash = base_convert($hash, 10, 36);
$id = substr($hash, 2, 9);
} else {
$id = substr(md5($this->id), 0, 9);
if (!$attachments instanceof \Illuminate\Support\Collection) {
$attachments = collect($attachments);
}
return $id;
}*/
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

@@ -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

@@ -44,6 +44,11 @@ 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')) {

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

@@ -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

@@ -43,38 +43,60 @@ class ModuleCheckLicenses extends Command
$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;
}
$params = [
'license' => $license,
'module_alias' => $module->getAlias(),
'url' => \App\Module::getAppUrl(),
];
$result = WpApi::checkLicense($params);
if (!empty($result['status']) && $result['status'] != 'valid') {
$msg = 'Module '.$module->getName().' has been deactivated due to invalid license: '.json_encode($result);
$this->error($module->getName().': '.$msg);
$data['license'] = $license;
$data['module_alias'] = $module->getAlias();
// Deactive module
\App\Module::deactiveModule($module->getAlias(), true);
$params['data'][] = $data;
}
// 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');
$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

@@ -46,14 +46,21 @@ class Kernel extends ConsoleKernel
$schedule->command('freescout:update-folder-counters')
->hourly();
$schedule->command('freescout:module-check-licenses')
->daily();
$app_key = config('app.key');
if ($app_key) {
$crc = crc32($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) {
@@ -180,6 +187,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');
}
}

View File

@@ -2,7 +2,10 @@
namespace App;
use App\Attachment;
use App\Customer;
use App\Mailbox;
use App\Folder;
use App\Thread;
use App\User;
use App\Events\UserAddedNote;
@@ -19,12 +22,17 @@ class Conversation extends Model
use Rememberable;
// This is obligatory.
public $rememberCacheDriver = 'array';
/**
* Max length of the preview.
*/
const PREVIEW_MAXLENGTH = 255;
/**
* Default subject length.
*/
const SUBJECT_LENGTH = 80;
/**
* Conversation reply undo timeout in seconds.
* Value has to be larger than close_after in fsFloatingAlertsInit.
@@ -47,7 +55,7 @@ class Conversation extends Model
*/
const TYPE_EMAIL = 1;
const TYPE_PHONE = 2;
const TYPE_CHAT = 3; // not used
const TYPE_CHAT = 3;
public static $types = [
self::TYPE_EMAIL => 'email',
@@ -125,6 +133,21 @@ class Conversation extends Model
self::SOURCE_TYPE_API => 'api',
];
/**
* Email history options.
*/
// const EMAIL_HISTORY_GLOBAL = 0;
// const EMAIL_HISTORY_NONE = 1;
// const EMAIL_HISTORY_LAST = 2;
// const EMAIL_HISTORY_FULL = 3;
public static $email_history_codes = [
'global',
'none',
'last',
'full',
];
/**
* Assignee.
*/
@@ -138,6 +161,7 @@ class Conversation extends Model
'customer',
'mailbox',
'status',
'state',
'subject',
'attachments',
'type',
@@ -167,7 +191,7 @@ class Conversation extends Model
*
* @var array
*/
public static $starred_conversation_ids = null;
public static $starred_conversation_ids = [];
/**
* Automatically converted into Carbon dates.
@@ -231,6 +255,15 @@ class Conversation extends Model
return $this->belongsTo('App\Mailbox');
}
/**
* Chached mailbox.
* @return [type] [description]
*/
public function mailbox_cached()
{
return $this->mailbox()->rememberForever();
}
/**
* Get the customer associated with this conversation (primaryCustomer).
*/
@@ -326,7 +359,7 @@ class Conversation extends Model
}
/**
* Get all published conversation thread in desc order.
* Get all published conversation threads in desc order.
*
* @return Collection
*/
@@ -366,10 +399,14 @@ class Conversation extends Model
*
* @return [type] [description]
*/
public function getLastReply()
public function getLastReply($include_phone_replies = false)
{
$types = [Thread::TYPE_CUSTOMER, Thread::TYPE_MESSAGE];
if ($include_phone_replies && $this->isPhone()) {
$types[] = Thread::TYPE_NOTE;
}
return $this->threads()
->whereIn('type', [Thread::TYPE_CUSTOMER, Thread::TYPE_MESSAGE])
->whereIn('type', $types)
->where('state', Thread::STATE_PUBLISHED)
->orderBy('created_at', 'desc')
->first();
@@ -445,6 +482,11 @@ class Conversation extends Model
return $this->status == self::STATUS_ACTIVE;
}
public function isSpam()
{
return $this->status == self::STATUS_SPAM;
}
/**
* Get status name.
*
@@ -491,6 +533,34 @@ class Conversation extends Model
}
}
/**
* Convert state code to name.
*
* @param int $status
*
* @return string
*/
public static function stateCodeToName($status)
{
switch ($status) {
case self::STATE_DRAFT:
return __('Draft');
break;
case self::STATE_PUBLISHED:
return __('Published');
break;
case self::STATE_DELETED:
return __('Deleted');
break;
default:
return '';
break;
}
}
/**
* Set conersation status and all related fields.
*
@@ -544,8 +614,12 @@ class Conversation extends Model
} else {
$folder = $this->folder;
}
$query = self::where('folder_id', $folder->id)
//$query = self::where('folder_id', $folder->id)->where('id', '<>', $this->id);
$query = self::getQueryByFolder($folder, \Auth::id())
->where('id', '<>', $this->id);
$query = \Eventy::filter('conversation.get_nearby_query', $query, $this, $mode, $folder);
$order_bys = $folder->getOrderByArray();
if ($mode != 'prev') {
@@ -717,7 +791,33 @@ class Conversation extends Model
*/
public static function sanitizeEmails($emails)
{
return \App\Misc\Mail::sanitizeEmails($emails);
// Create customers if needed: Test <test1@example.com>
if (is_array($emails)) {
foreach ($emails as $i => $email) {
preg_match("/^(.+)\s+([^\s]+)$/", $email ?? '', $m);
if (count($m) == 3) {
$customer_name = trim($m[1]);
$email_address = trim($m[2]);
if ($customer_name) {
preg_match("/^([^\s]+)\s+([^\s]+)$/", $customer_name, $m_customer);
$customer_data = [];
if (count($m_customer) == 3) {
$customer_data['first_name'] = $m_customer[1];
$customer_data['last_name'] = $m_customer[2];
} else {
$customer_data['first_name'] = $customer_name;
}
Customer::create($email_address, $customer_data);
}
$emails[$i] = $email_address;
}
}
}
return \MailHelper::sanitizeEmails($emails);
}
/**
@@ -735,7 +835,7 @@ class Conversation extends Model
/**
* Static function for retrieving URL.
*
*
* @param [type] $id [description]
* @param [type] $folder_id [description]
* @param [type] $thread_id [description]
@@ -810,6 +910,7 @@ class Conversation extends Model
}
} else {
// todo: check ConversationFolder here
return \Eventy::filter('conversation.is_in_folder_allowed', false, $folder, $this);
}
return false;
@@ -829,14 +930,16 @@ class Conversation extends Model
return false;
}
}
$mailbox_id = $this->mailbox_id;
// Get ids of all the conversations starred by user and cache them
if (self::$starred_conversation_ids === null) {
$mailbox_id = $this->mailbox_id;
self::$starred_conversation_ids = self::getUserStarredConversationIds($mailbox_id, $user_id);
if (!isset(self::$starred_conversation_ids[$mailbox_id])) {
self::$starred_conversation_ids[$mailbox_id] = self::getUserStarredConversationIds($mailbox_id, $user_id);
}
if (self::$starred_conversation_ids) {
return in_array($this->id, self::$starred_conversation_ids);
if (self::$starred_conversation_ids[$mailbox_id]) {
return in_array($this->id, self::$starred_conversation_ids[$mailbox_id]);
} else {
return false;
}
@@ -955,7 +1058,9 @@ class Conversation extends Model
*/
public function getSignatureProcessed($data = [], $escape = false)
{
return $this->replaceTextVars($this->mailbox->signature, $data, $escape);
$replaced_text = $this->replaceTextVars( $this->mailbox->signature, $data, $escape );
return \Eventy::filter( 'conversation.signature_processed', $replaced_text, $this, $data, $escape );
}
/**
@@ -963,7 +1068,7 @@ class Conversation extends Model
*/
public function replaceTextVars($text, $data = [], $escape = false)
{
if (!\App\Misc\Mail::hasVars($text)) {
if (!\MailHelper::hasVars($text)) {
return $text;
}
@@ -1005,6 +1110,10 @@ class Conversation extends Model
}
}
if (!$customer_email) {
$customer_email = $customer->getMainEmail();
}
$prev_customer_id = $this->customer_id;
$prev_customer_email = $this->customer_email;
@@ -1041,16 +1150,57 @@ class Conversation extends Model
{
$prev_mailbox = $this->mailbox;
foreach ($this->folders as $folder) {
// Process indirect folders.
if (!in_array($folder->type, Folder::$indirect_types)) {
continue;
}
// Remove conversation from the folder.
$this->removeFromFolder($folder->type, $folder->user_id);
if ($folder->type == Folder::TYPE_STARRED) {
self::clearStarredByUserCache($folder->user_id, $this->mailbox_id);
}
}
// We don't know how to replace $this->mailbox object.
$this->mailbox_id = $mailbox->id;
// Check assignee.
if ($this->user_id && !in_array($this->user_id, $mailbox->userIdsHavingAccess())) {
// Assign conversation to the user who moved it.
$conversation->user_id = $user->id;
$this->user_id = $user->id;
}
$this->updateFolder($mailbox);
$this->save();
foreach ($this->folders as $folder) {
// Process indirect folders.
if (!in_array($folder->type, Folder::$indirect_types)) {
continue;
}
// If user has access to the new mailbox,
// move conversation to the same folder in the new mailbox.
if ($folder->user_id) {
if ($folder->user->hasAccessToMailbox($mailbox->id)) {
foreach ($mailbox->folders as $mailbox_folder) {
if ($mailbox_folder->type == $folder->type) {
$this->addToFolder($folder->type, $folder->user_id);
if ($folder->type == Folder::TYPE_STARRED) {
self::clearStarredByUserCache($folder->user_id, $mailbox->id);
}
break;
}
}
}
} else {
foreach ($mailbox->folders as $mailbox_folder) {
if ($mailbox_folder->type == $folder->type) {
$this->addToFolder($folder->type, $folder->user_id);
break;
}
}
}
}
// Add record to the conversation history.
Thread::create($this, Thread::TYPE_LINEITEM, '', [
'created_by_user_id' => $user->id,
@@ -1066,6 +1216,57 @@ class Conversation extends Model
$prev_mailbox->updateFoldersCounters();
$mailbox->updateFoldersCounters();
\Eventy::action('conversation.moved', $this, $user, $prev_mailbox);
return true;
}
/**
* Merge conversations
*/
public function mergeConversations($merge_conversation, $user)
{
// Move all threads from old to new conversation.
foreach ($merge_conversation->threads as $thread) {
$thread->conversation_id = $this->id;
$thread->setMeta(Thread::META_PREV_CONVERSATION, $merge_conversation->id);
$thread->save();
}
// Add record to the new conversation.
Thread::create($this, Thread::TYPE_LINEITEM, '', [
'created_by_user_id' => $user->id,
'user_id' => $this->user_id,
'state' => Thread::STATE_PUBLISHED,
'action_type' => Thread::ACTION_TYPE_MERGED,
'source_via' => Thread::PERSON_USER,
'source_type' => Thread::SOURCE_TYPE_WEB,
'customer_id' => $this->customer_id,
'meta' => [Thread::META_MERGED_WITH_CONV => $merge_conversation->id],
]);
// Add record to the old conversation.
Thread::create($merge_conversation, Thread::TYPE_LINEITEM, '', [
'created_by_user_id' => $user->id,
'user_id' => $merge_conversation->user_id,
'state' => Thread::STATE_PUBLISHED,
'action_type' => Thread::ACTION_TYPE_MERGED,
'source_via' => Thread::PERSON_USER,
'source_type' => Thread::SOURCE_TYPE_WEB,
'customer_id' => $merge_conversation->customer_id,
'meta' => [Thread::META_MERGED_INTO_CONV => $this->id],
]);
// Delete old conversation.
$merge_conversation->deleteToFolder($user);
// Update counters.
$this->mailbox->updateFoldersCounters();
if ($this->mailbox_id != $merge_conversation->mailbox_id) {
$merge_conversation->mailbox->updateFoldersCounters();
}
\Eventy::action('conversation.merged', $this, $merge_conversation, $user);
return true;
}
@@ -1075,7 +1276,7 @@ class Conversation extends Model
public static function loadUsers($conversations)
{
$user_ids = $conversations->pluck('user_id')->unique()->toArray();
if (!$user_ids) {
if (!$user_ids || (count($user_ids) == 1 && empty($user_ids[0]))) {
return;
}
@@ -1127,6 +1328,35 @@ class Conversation extends Model
}
}
/**
* Load mailboxes.
*/
public static function loadMailboxes($conversations)
{
$ids = $conversations->pluck('mailbox_id')->unique()->toArray();
if (!$ids) {
return;
}
$mailboxes = Mailbox::whereIn('id', $ids)->get();
if (!$mailboxes) {
return;
}
foreach ($conversations as $conversation) {
if (empty($conversation->mailbox_id)) {
continue;
}
foreach ($mailboxes as $mailbox) {
if ($mailbox->id == $conversation->mailbox_id) {
$conversation->mailbox = $mailbox;
continue 2;
}
}
}
}
public function getSubject()
{
if ($this->subject) {
@@ -1139,12 +1369,16 @@ class Conversation extends Model
/**
* Add conversation to folder via conversation_folder table.
*/
public function addToFolder($folder_type)
public function addToFolder($folder_type, $user_id = null)
{
// Find folder
$folder = Folder::where('mailbox_id', $this->mailbox_id)
->where('type', $folder_type)
->first();
// Find folder.
$folder_query = Folder::where('mailbox_id', $this->mailbox_id)
->where('type', $folder_type);
if ($user_id) {
$folder_query->where('user_id', $user_id);
}
$folder = $folder_query->first();
if (!$folder) {
return false;
}
@@ -1166,20 +1400,32 @@ class Conversation extends Model
// 'conversation_id' => $this->id,
// ];
// ConversationFolder::updateOrCreate($values, $values);
return true;
}
public function removeFromFolder($folder_type)
/**
* When removing from Starred folder, don't forget to clear cache using clearStarredByUserCache()
*/
public function removeFromFolder($folder_type, $user_id = null)
{
// Find folder
$folder = Folder::where('mailbox_id', $this->mailbox_id)
->where('type', $folder_type)
->first();
$folder_query = Folder::where('mailbox_id', $this->mailbox_id)
->where('type', $folder_type);
if ($user_id) {
$folder_query->where('user_id', $user_id);
}
$folder = $folder_query->first();
if (!$folder) {
return false;
}
$this->folders()->detach($folder->id);
$folder->updateCounters();
return true;
}
/**
@@ -1217,10 +1463,17 @@ class Conversation extends Model
*
* @return [type] [description]
*/
public function getWaitingSince($folder)
public function getWaitingSince($folder = null)
{
if (!$folder) {
$folder = $this->folder;
}
$waiting_since_field = $folder->getWaitingSinceField();
if ($waiting_since_field) {
// For phone conversations.
if (empty($this->$waiting_since_field)) {
$waiting_since_field = 'updated_at';
}
return \App\User::dateDiffForHumans($this->$waiting_since_field);
} else {
return '';
@@ -1272,7 +1525,7 @@ class Conversation extends Model
$mailbox = $this->mailbox;
}
$customer_emails = [$this->customer_email];
if (strstr($this->customer_email, ',')) {
if (strstr($this->customer_email ?? '', ',')) {
// customer_email contains mutiple addresses (when new conversation for multiple recipients created)
$customer_emails = explode(',', $this->customer_email);
}
@@ -1287,6 +1540,14 @@ class Conversation extends Model
return ($this->type == self::TYPE_PHONE);
}
/**
* Is it as chat conversation.
*/
public function isChat()
{
return ($this->type == self::TYPE_CHAT);
}
/**
* Get information on viewers for conversation table.
*/
@@ -1342,6 +1603,10 @@ class Conversation extends Model
public function changeStatus($new_status, $user, $create_thread = true)
{
if (!array_key_exists($new_status, self::$statuses)) {
return;
}
$prev_status = $this->status;
$this->setStatus($new_status, $user);
@@ -1371,24 +1636,26 @@ class Conversation extends Model
public function changeUser($new_user_id, $user, $create_thread = true)
{
$prev_user_id = $this->user_id;
$this->setUser($new_user_id);
$this->save();
// Create lineitem thread
$thread = new Thread();
$thread->conversation_id = $this->id;
$thread->user_id = $this->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;
// todo: this need to be changed for API
$thread->source_type = Thread::SOURCE_TYPE_WEB;
$thread->customer_id = $this->customer_id;
$thread->created_by_user_id = $user->id;
$thread->save();
if ($create_thread) {
// Create lineitem thread
$thread = new Thread();
$thread->conversation_id = $this->id;
$thread->user_id = $this->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;
// todo: this need to be changed for API
$thread->source_type = Thread::SOURCE_TYPE_WEB;
$thread->customer_id = $this->customer_id;
$thread->created_by_user_id = $user->id;
$thread->save();
}
event(new ConversationUserChanged($this, $user));
\Eventy::action('conversation.user_changed', $this, $user, $prev_user_id);
@@ -1397,7 +1664,7 @@ class Conversation extends Model
public function deleteToFolder($user)
{
$folder_id = $this->getCurrentFolder();
$this->state = Conversation::STATE_DELETED;
$this->user_updated_at = date('Y-m-d H:i:s');
$this->updateFolder();
@@ -1423,6 +1690,8 @@ class Conversation extends Model
// Recalculate only old and new folders
$this->mailbox->updateFoldersCounters();
\Eventy::action('conversation.deleted', $this, $user);
}
public function deleteForever()
@@ -1478,7 +1747,7 @@ class Conversation extends Model
}
}
public function forward($user, $body, $to = '', $data = [])
public function forward($user, $body, $to = '', $data = [], $include_attachments = false)
{
// Create thread
$thread = Thread::create($this, $data['type'] ?? Thread::TYPE_NOTE, $body, $data, false);
@@ -1512,9 +1781,6 @@ class Conversation extends Model
$forwarded_conversation->last_reply_at = $now;
$forwarded_conversation->last_reply_from = Conversation::PERSON_USER;
$forwarded_conversation->user_updated_at = $now;
// if ($attachments_info['has_attachments']) {
// $forwarded_conversation->has_attachments = true;
// }
$forwarded_conversation->updateFolder();
$forwarded_conversation->save();
@@ -1524,7 +1790,7 @@ class Conversation extends Model
$thread->subtype = Thread::SUBTYPE_FORWARD;
$thread->setMeta('forward_child_conversation_number', $forwarded_conversation->number);
$thread->setMeta('forward_child_conversation_id', $forwarded_conversation->id);
$thread->save();
// Save forwarded thread.
@@ -1540,6 +1806,52 @@ class Conversation extends Model
$forwarded_thread->setMeta('forward_parent_thread_id', $thread->id);
$forwarded_thread->save();
// Add attachments if needed.
if ($include_attachments) {
$replies = $this->getReplies();
$has_attachments = false;
foreach ($replies as $reply_thread) {
$thread_has_attachments = false;
foreach ($reply_thread->attachments as $attachment) {
$new_attachment = $attachment->replicate();
$new_attachment->thread_id = $forwarded_thread->id;
// We need to copy attachment file, because conversations
// can be deleted along with attachments.
$new_attachment->save();
try {
$attachment_file = new \Illuminate\Http\UploadedFile(
$attachment->getLocalFilePath(), $attachment->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();
$has_attachments = true;
$thread_has_attachments = true;
}
} catch (\Exception $e) {
\Helper::logException($e);
}
}
if ($thread_has_attachments) {
$forwarded_thread->has_attachments = true;
$forwarded_thread->save();
}
}
if ($has_attachments) {
$forwarded_conversation->has_attachments = true;
$forwarded_conversation->save();
}
}
// Update folders counters
$this->mailbox->updateFoldersCounters();
@@ -1550,6 +1862,287 @@ class Conversation extends Model
\Eventy::action('conversation.user_forwarded', $this, $thread, $forwarded_conversation, $forwarded_thread);
}
// public function getEmailHistoryCode()
// {
// return self::$email_history_codes[(int)$this->email_history] ?? 'global';
// }
public static function getEmailHistoryName($code) {
$label = '';
switch ($code) {
case 'global':
$label = __('Default');
$label .= ' ('.self::getEmailHistoryName(config('app.email_conv_history')).')';
break;
case 'none':
$label = __('Do not include previous messages');
break;
case 'last':
$label = __('Include the last message');
break;
case 'full':
$label = __('Send full conversation history');
break;
}
return $label;
}
/**
* Create conversation.
*
* $threads should go from old to new.
*/
public static function create($data, $threads, $customer)
{
// Detect source_via.
$source_via = $data['source_via'] ?? 0;
if (!$source_via && !empty($threads[0])) {
if (!empty($threads[0]['type']) && $threads[0]['type'] == Thread::TYPE_CUSTOMER) {
$source_via = self::PERSON_CUSTOMER;
} else {
$source_via = self::PERSON_USER;
}
}
$conversation = new Conversation();
$conversation->type = $data['type'];
$conversation->subject = $data['subject'];
$conversation->mailbox_id = $data['mailbox_id'];
$conversation->source_via = $source_via;
$conversation->source_type = $data['source_type'];
$conversation->customer_id = $customer->id;
$conversation->customer_email = $customer->getMainEmail().'';
$conversation->state = $data['state'] ?? Conversation::STATE_PUBLISHED;
$conversation->imported = (int)($data['imported'] ?? false);
$conversation->closed_at = $data['closed_at'] ?? null;
$conversation->channel = $data['channel'] ?? null;
$conversation->preview = '';
// Phone conversation is always pending.
if ($conversation->isPhone()) {
$conversation->status = Conversation::STATUS_PENDING;
}
// Set assignee
$conversation->user_id = null;
if (!empty($data['user_id'])) {
$user_assignee = User::find($data['user_id']);
if ($user_assignee) {
$conversation->user_id = $user_assignee->id;
}
}
$conversation->updateFolder();
$conversation->save();
// Create threads.
$threads = array_reverse($threads);
$thread_created = false;
$last_customer_id = null;
$thread_result = null;
foreach ($threads as $thread) {
$thread['conversation_id'] = $conversation->id;
if ($conversation->imported) {
$thread['imported'] = true;
}
if (!empty($data['status'])) {
$thread['status'] = $data['status'];
}
$thread_result = Thread::createExtended($thread, $conversation, $customer, false);
if ($thread_result) {
$thread_created = true;
}
}
// If no threads created, delete conversation
if (!$thread_created) {
$conversation->delete();
return false;
}
// Restore customer if needed.
// if ($last_customer_id && $last_customer_id != $customer->id) {
// // Otherwise it does not save.
// $conversation = $conversation->fresh();
// $conversation->customer_id = $customer->id;
// $conversation->customer_email = $customer->getMainEmail();
// $conversation->save();
// }
// Update folders counters
$conversation->mailbox->updateFoldersCounters();
return [
'conversation' => $conversation,
'thread' => $thread_result
];
}
public function getChannelName()
{
return self::channelCodeToName($this->channel);
}
public static function channelCodeToName($channel)
{
return \Eventy::filter('channel.name', '', $channel);
}
public static function subjectFromText($text)
{
return \Helper::textPreview($text, self::SUBJECT_LENGTH);
}
public static function refreshConversations($conversation, $thread)
{
\App\Events\RealtimeConvNewThread::dispatchSelf($thread);
\App\Events\RealtimeMailboxNewThread::dispatchSelf($conversation->mailbox_id);
}
public static function getConvTableSorting($request = null)
{
if (!$request) {
$request = request();
}
$result = [
'sort_by' => 'date',
'order' => 'desc',
];
if (
!empty($request->sorting['sort_by']) && !empty($request->sorting['order']) &&
in_array($request->sorting['sort_by'], ['subject', 'number', 'date']) &&
in_array($request->sorting['order'], ['asc', 'desc'])
) {
$result['sort_by'] = $request->sorting['sort_by'];
$result['order'] = $request->sorting['order'];
}
return $result;
}
public static function search($q, $filters, $user = null, $query_conversations = null)
{
$mailbox_ids = [];
// Get IDs of mailboxes to which user has access
if (empty($filters['mailbox'])) {
$mailbox_ids = $user->mailboxesIdsCanView();
}
// Like is case insensitive.
$like = '%'.mb_strtolower($q).'%';
if (!$query_conversations) {
$query_conversations = Conversation::select('conversations.*');
}
// https://github.com/laravel/framework/issues/21242
// https://github.com/laravel/framework/pull/27675
$query_conversations->groupby('conversations.id');
if ($mailbox_ids) {
$query_conversations->whereIn('conversations.mailbox_id', $mailbox_ids);
}
if ($q) {
$query_conversations->where(function ($query) use ($like, $filters, $q) {
$query->where('conversations.subject', 'like', $like)
->orWhere('conversations.customer_email', 'like', $like)
->orWhere('conversations.number', (int)$q)
->orWhere('conversations.id', (int)$q)
->orWhere('threads.body', 'like', $like)
->orWhere('threads.from', 'like', $like)
->orWhere('threads.to', 'like', $like)
->orWhere('threads.cc', 'like', $like)
->orWhere('threads.bcc', 'like', $like);
$query = \Eventy::filter('search.conversations.or_where', $query, $filters, $q);
});
}
// Apply search filters.
if (!empty($filters['assigned'])) {
$query_conversations->where('conversations.user_id', $filters['assigned']);
}
if (!empty($filters['customer'])) {
$customer_id = $filters['customer'];
$query_conversations->where(function ($query) use ($customer_id) {
$query->where('conversations.customer_id', '=', $customer_id)
->orWhere('threads.created_by_customer_id', '=', $customer_id);
});
}
if (!empty($filters['mailbox'])) {
$query_conversations->where('conversations.mailbox_id', '=', $filters['mailbox']);
}
if (!empty($filters['status'])) {
if (count($filters['status']) == 1) {
// = is faster than IN.
$query_conversations->where('conversations.status', '=', $filters['status'][0]);
} else {
$query_conversations->whereIn('conversations.status', $filters['status']);
}
}
if (!empty($filters['state'])) {
if (count($filters['state']) == 1) {
// = is faster than IN.
$query_conversations->where('conversations.state', '=', $filters['state'][0]);
} else {
$query_conversations->whereIn('conversations.state', $filters['state']);
}
}
if (!empty($filters['subject'])) {
$query_conversations->where('conversations.subject', 'like', '%'.mb_strtolower($filters['subject']).'%');
}
if (!empty($filters['attachments'])) {
$has_attachments = ($filters['attachments'] == 'yes' ? true : false);
$query_conversations->where('conversations.has_attachments', '=', $has_attachments);
}
if (!empty($filters['type'])) {
$query_conversations->where('conversations.type', '=', $filters['type']);
}
if (!empty($filters['body'])) {
$query_conversations->where('threads.body', 'like', '%'.mb_strtolower($filters['body']).'%');
}
if (!empty($filters['number'])) {
$query_conversations->where('conversations.number', '=', $filters['number']);
}
if (!empty($filters['following'])) {
if ($filters['following'] == 'yes') {
$query_conversations->join('followers', function ($join) {
$join->on('followers.conversation_id', '=', 'conversations.id');
$join->where('followers.user_id', auth()->user()->id);
});
}
}
if (!empty($filters['id'])) {
$query_conversations->where('conversations.id', '=', $filters['id']);
}
if (!empty($filters['after'])) {
$query_conversations->where('conversations.created_at', '>=', date('Y-m-d 00:00:00', strtotime($filters['after'])));
}
if (!empty($filters['before'])) {
$query_conversations->where('conversations.created_at', '<=', date('Y-m-d 23:59:59', strtotime($filters['before'])));
}
// Join threads if needed
if (!strstr($query_conversations->toSql(), '`threads`.`conversation_id`')) {
$query_conversations->join('threads', function ($join) {
$join->on('conversations.id', '=', 'threads.conversation_id');
});
}
$query_conversations = \Eventy::filter('search.conversations.apply_filters', $query_conversations, $filters, $q);
$query_conversations->orderBy('conversations.last_reply_at', 'DESC');
return $query_conversations;
}
// /**
// * Get conversation meta data as array.
// */

View File

@@ -2,7 +2,10 @@
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
@@ -11,6 +14,10 @@ class Customer extends Model
// This is obligatory.
public $rememberCacheDriver = 'array';
const PHOTO_DIRECTORY = 'customers';
const PHOTO_SIZE = 64; // px
const PHOTO_QUALITY = 77;
/**
* Genders.
*/
@@ -43,10 +50,10 @@ class Customer extends Model
public static $phone_types = [
self::PHONE_TYPE_WORK => 'work',
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',
self::PHONE_TYPE_OTHER => 'other',
];
/**
@@ -78,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;
@@ -138,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',
@@ -154,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',
@@ -167,6 +177,13 @@ class Customer extends Model
self::SOCIAL_TYPE_OTHER => 'Other',
];
/**
* Search filters.
*/
public static $search_filters = [
'mailbox',
];
/**
* Countries list.
*/
@@ -423,6 +440,10 @@ class Customer extends Model
'ZW' => 'Zimbabwe',
];
protected $casts = [
'meta' => 'array',
];
/**
* Attributes which are not fillable using fill() method.
*/
@@ -433,12 +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', 'chats'];
protected $json_fields = ['phones', 'websites', 'social_profiles'];
/**
* Get customer emails.
@@ -472,6 +493,14 @@ class Customer extends Model
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');
}
/**
* Get customer full name.
*
@@ -598,10 +627,17 @@ class Customer extends Model
*
* @return array
*/
public function getPhones()
public function getPhones($dummy_if_empty = false)
{
if ($this->phones) {
return json_decode($this->phones, true);
$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 [];
}
@@ -639,9 +675,13 @@ class Customer extends Model
public static function formatPhones(array $phones_array)
{
$phones = [];
foreach ($phones_array as $phone) {
if (is_array($phone)) {
if (!empty($phone['value']) && !empty($phone['type']) && in_array($phone['type'], array_keys(self::$phone_types))) {
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'],
@@ -663,10 +703,26 @@ class Customer extends Model
*/
public function addPhone($phone, $type = self::PHONE_TYPE_WORK)
{
$this->setPhones(array_merge(
$this->getPhones(),
[['value' => $phone, 'type' => $type]]
));
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();
}
/**
@@ -674,10 +730,17 @@ class Customer extends Model
*
* @return array
*/
public function getSocialProfiles()
public function getSocialProfiles($dummy_if_empty = false)
{
if ($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 [];
}
@@ -690,8 +753,9 @@ class Customer extends Model
*/
public function getWebsites($dummy_if_empty = false)
{
if ($this->websites) {
return json_decode($this->websites, true);
$websites = json_decode($this->websites ?? '', true);
if (is_array($websites) && count($websites)) {
return $websites;
} elseif ($dummy_if_empty) {
return [''];
} else {
@@ -710,6 +774,12 @@ class Customer extends Model
foreach ($websites_array as $key => $value) {
// 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;
}
@@ -723,7 +793,81 @@ class Customer extends Model
*/
public function addWebsite($website)
{
$this->setWebsites(array_merge($this->getWebsites(), [$value]));
$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);
}
/**
@@ -736,6 +880,8 @@ class Customer extends Model
*/
public static function create($email, $data = [])
{
$new = false;
$email = Email::sanitizeEmail($email);
if (!$email) {
return null;
@@ -744,6 +890,12 @@ class Customer extends Model
if ($email_obj) {
$customer = $email_obj->customer;
// 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'];
@@ -756,6 +908,8 @@ class Customer extends Model
$customer = new self();
$email_obj = new Email();
$email_obj->email = $email;
$new = true;
}
// Set empty fields
@@ -763,9 +917,31 @@ class Customer extends Model
$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;
@@ -778,9 +954,24 @@ class Customer extends Model
{
$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.
$this->fill($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.
@@ -793,23 +984,80 @@ class Customer extends Model
}
// Set JSON values.
if (!empty($data['phone'])) {
$this->addPhone($data['phone']);
}
foreach ($data as $key => $value) {
if (!in_array($key, $this->json_fields)) {
if (!in_array($key, $this->json_fields) && $key != 'emails') {
continue;
}
// todo: setChats, setSocialProfiles
if ($key == 'phones') {
foreach ($value as $phone_value) {
$this->addPhone($phone_value);
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') {
$this->addWebsite($value);
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();
}
@@ -818,14 +1066,18 @@ class Customer extends Model
}
/**
* Create a customer, email is not required.
* For phone conversations.
*/
public static function createWithoutEmail($data = [])
{
$customer = new self();
$customer->fill($data);
$customer->setData($data);
$customer->save();
\Eventy::action('customer.created', $customer);
return $customer;
}
@@ -839,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.
*
@@ -852,11 +1114,6 @@ class Customer extends Model
return $date->format($format);
}
public function getPhotoUrl()
{
return asset('/img/default-avatar.png');
}
/**
* Get full representation of customer.
*/
@@ -911,14 +1168,19 @@ class Customer extends Model
->get()
->toArray();
foreach ($data as $values) {
foreach ($list as $email) {
// Dummy customer.
$customer = new Customer();
$customer->email = $values['email'];
$customer->first_name = $values['first_name'];
$customer->last_name = $values['last_name'];
$customer->email = $email;
$result[$values['email']] = $customer->getNameAndEmail();
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;
@@ -952,4 +1214,193 @@ class Customer extends Model
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 \Eventy::filter('customer.default_avatar', '/img/default-avatar.png', $this);
}
}
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;
}
public static function getPhoneTypeName($code)
{
$phone_types = [
self::PHONE_TYPE_WORK => __('Work'),
self::PHONE_TYPE_HOME => __('Home'),
self::PHONE_TYPE_OTHER => __('Other'),
self::PHONE_TYPE_MOBILE => __('Mobile'),
self::PHONE_TYPE_FAX => __('Fax'),
self::PHONE_TYPE_PAGER => __('Pager'),
];
return $phone_types[$code] ?? '';
}
public static function isDefaultPhoneType($code)
{
return (self::PHONE_TYPE_WORK == $code);
}
}

View File

@@ -52,7 +52,7 @@ 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);
@@ -69,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

@@ -88,7 +88,11 @@ class RealtimeMailboxNewThread implements ShouldBroadcastNow
return [];
}
$folder = Folder::find(Conversation::getFolderParam());
$folder = null;
$foler_id = Conversation::getFolderParam();
if ($foler_id) {
$folder = Folder::find($foler_id);
}
// Just in case.
if (!$folder) {
$folder = new Folder();

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',
];
/**
@@ -152,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:
@@ -161,6 +160,7 @@ class Folder extends Model
case self::TYPE_CLOSED:
$order_by = [['closed_at' => 'desc']];
break;
case self::TYPE_SPAM:
$order_by = [['last_reply_at' => 'desc']];
@@ -169,6 +169,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;
@@ -205,8 +229,8 @@ class Folder extends Model
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)
@@ -248,7 +272,14 @@ class Folder extends Model
*/
public function getCount($folders = [])
{
if (\Eventy::filter('folder.counter', self::COUNTER_ACTIVE, $this, $folders) == self::COUNTER_TOTAL || $this->type == self::TYPE_STARRED || $this->type == self::TYPE_DRAFTS) {
$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);
@@ -262,10 +293,14 @@ class Folder extends Model
{
$active_count = $this->active_count;
if ($this->type == self::TYPE_ASSIGNED) {
if ($folders) {
$mine_folder = $folders->firstWhere('type', self::TYPE_MINE);
} else {
$mine_folder = $this->mailbox->folders()->where('type', self::TYPE_MINE)->first();
$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) {
@@ -339,4 +374,74 @@ class Folder extends Model
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;
}
/**
* Get meta value.
*/
public function getMeta($key, $default = null)
{
$metas = $this->meta;
if (isset($metas[$key])) {
return $metas[$key];
} else {
return $default;
}
}
/**
* Set meta value.
*/
public function setMeta($key, $value)
{
$metas = $this->meta;
$metas[$key] = $value;
$this->meta = $metas;
}
/**
* Unset thread meta value.
*/
public function unsetMeta($key)
{
$metas = $this->meta;
if (isset($metas[$key])) {
unset($metas[$key]);
$this->meta = $metas;
}
}
}

View File

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

572
app/Http/Controllers/ConversationsController.php Normal file → Executable file
View File

@@ -44,12 +44,20 @@ class ConversationsController extends Controller
public function view(Request $request, $id)
{
$conversation = Conversation::findOrFail($id);
$this->authorize('view', $conversation);
$this->authorize('viewCached', $conversation);
$mailbox = $conversation->mailbox;
$customer = $conversation->customer_cached;
$user = auth()->user();
// To let other parts of the app easily access.
\Helper::setGlobalEntity('conversation', $conversation);
\Helper::setGlobalEntity('mailbox', $mailbox);
if ($user->isAdmin()) {
$mailbox->fetchUserSettings($user->id);
}
// Mark notifications as read
if (!empty($request->mark_as_read)) {
$mark_read_result = $user->unreadNotifications()->where('id', $request->mark_as_read)->update(['read_at' => now()]);
@@ -105,7 +113,8 @@ class ConversationsController extends Controller
return redirect()->away($conversation->url($folder->id));
}
$after_send = $conversation->mailbox->getUserSettings($user->id)->after_send;
//$after_send = $conversation->mailbox->getUserSettings($user->id)->after_send;
$after_send = $user->mailboxSettings($conversation->mailbox_id)->after_send;
// Detect customers and emails to which user can reply
$to_customers = [];
@@ -170,7 +179,8 @@ class ConversationsController extends Controller
$prev_conversations = $mailbox->conversations()
->where('customer_id', $customer->id)
->where('id', '<>', $conversation->id)
->where('status', '!=', Thread::STATUS_SPAM)
->where('status', '!=', Conversation::STATUS_SPAM)
->where('state', Conversation::STATE_PUBLISHED)
//->limit(self::PREV_CONVERSATIONS_LIMIT)
->orderBy('created_at', 'desc')
->paginate(self::PREV_CONVERSATIONS_LIMIT);
@@ -238,7 +248,7 @@ class ConversationsController extends Controller
}
// Show replying first.
usort($viewers, function($a, $b) {
return $a['replying'] < $b['replying'];
return $b['replying'] <=> $a['replying'];
});
}
@@ -258,7 +268,7 @@ class ConversationsController extends Controller
'to_customers' => $to_customers,
'prev_conversations' => $prev_conversations,
'cc' => $conversation->getCcArray($exclude_array),
'bcc' => $conversation->getBccArray($exclude_array),
'bcc' => [], //$conversation->getBccArray($exclude_array),
// Data for creating a phone conversation.
'name' => $name,
'phone' => $phone,
@@ -282,6 +292,7 @@ class ConversationsController extends Controller
$folder = $mailbox->folders()->where('type', Folder::TYPE_DRAFTS)->first();
// todo: use $user->mailboxSettings()
$after_send = $mailbox->getUserSettings(auth()->user()->id)->after_send;
// Create conversation from thread
@@ -308,6 +319,13 @@ class ConversationsController extends Controller
}
}
$to = [];
$prefill_to = \App\Email::sanitizeEmail($request->get('to'));
if ($prefill_to) {
$to = [$prefill_to => $prefill_to];
}
return view('conversations/create', [
'conversation' => $conversation,
'thread' => $thread,
@@ -315,7 +333,7 @@ class ConversationsController extends Controller
'folder' => $folder,
'folders' => $mailbox->getAssesibleFolders(),
'after_send' => $after_send,
'to' => [],
'to' => $to,
]);
}
@@ -479,6 +497,7 @@ class ConversationsController extends Controller
$is_note = true;
}
// Conversation type.
$type = Conversation::TYPE_EMAIL;
if (!empty($request->type)) {
$type = (int)$request->type;
@@ -606,12 +625,20 @@ class ConversationsController extends Controller
if ((int) $request->status != (int) $conversation->status) {
$status_changed = true;
}
if (!empty($request->subject)) {
$conversation->subject = $request->subject;
}
// When switching from regular message to phone and message sent
// without saving a draft type need to be saved here.
if ($conversation->type == Conversation::TYPE_EMAIL && $type == Conversation::TYPE_PHONE) {
$conversation->type = $type;
}
}
if ($attachments_info['has_attachments']) {
$conversation->has_attachments = true;
}
// Customer can be empty in existing conversation if this is a draft.
$customer_email = '';
$customer = null;
@@ -622,6 +649,9 @@ class ConversationsController extends Controller
$customer_email = $phone_customer_data['customer_email'];
$customer = $phone_customer_data['customer'];
if (!$conversation->customer_id) {
$conversation->customer_id = $customer->id;
}
} else {
// Email.
if (!empty($to_array)) {
@@ -641,6 +671,11 @@ class ConversationsController extends Controller
$prev_status = $conversation->status;
$conversation->status = $request->status;
if ($prev_status != $conversation->status && $conversation->status == Conversation::STATUS_CLOSED) {
$conversation->closed_by_user_id = $user->id;
$conversation->closed_at = date('Y-m-d H:i:s');
}
// We need to set state, as it may have been a draft
$conversation->state = Conversation::STATE_PUBLISHED;
// Set assignee
@@ -660,7 +695,7 @@ class ConversationsController extends Controller
// List of emails.
$to_list = [];
if ($is_forward) {
$to = $request->to_email;
$to = $request->to_email[0];
} else {
if (!empty($request->to)) {
// When creating a new conversation, to is a list of emails.
@@ -686,6 +721,10 @@ class ConversationsController extends Controller
$conversation->last_reply_from = Conversation::PERSON_USER;
$conversation->user_updated_at = $now;
}
if ($conversation->isPhone() && $is_note) {
$conversation->last_reply_at = $now;
$conversation->last_reply_from = Conversation::PERSON_USER;
}
$conversation->updateFolder();
if ($from_draft) {
// Increment number of replies in conversation
@@ -701,6 +740,8 @@ class ConversationsController extends Controller
}
// Fire events
\Eventy::action('conversation.send_reply_save', $conversation, $request);
if (!$new) {
if ($status_changed) {
event(new ConversationStatusChanged($conversation));
@@ -756,51 +797,83 @@ class ConversationsController extends Controller
if (!empty($request->saved_reply_id)) {
$thread->saved_reply_id = $request->saved_reply_id;
}
if ($is_forward) {
// Create forwarded conversation.
$forwarded_conversation = $conversation->replicate();
$forwarded_conversation->type = Conversation::TYPE_EMAIL;
$forwarded_conversation->setPreview($thread->body);
$forwarded_conversation->created_by_user_id = auth()->user()->id;
$forwarded_conversation->source_via = Conversation::PERSON_USER;
$forwarded_conversation->source_type = Conversation::SOURCE_TYPE_WEB;
$forwarded_conversation->threads_count = 0; // Counter will be incremented in ThreadObserver.
$forwarded_customer = Customer::create($customer_email);
$forwarded_conversation->customer_id = $forwarded_customer->id;
$forwarded_conversation->customer_email = $customer_email;
$forwarded_conversation->subject = 'Fwd: '.$forwarded_conversation->subject;
$forwarded_conversation->setCc(array_merge(Conversation::sanitizeEmails($request->cc), [$to]));
$forwarded_conversation->setBcc($request->bcc);
$forwarded_conversation->last_reply_at = $now;
$forwarded_conversation->last_reply_from = Conversation::PERSON_USER;
$forwarded_conversation->user_updated_at = $now;
if ($attachments_info['has_attachments']) {
$forwarded_conversation->has_attachments = true;
}
$forwarded_conversation->updateFolder();
$forwarded_conversation->save();
$forwarded_conversations = [];
$forwarded_threads = [];
$forwarded_thread = $thread->replicate();
if ($is_forward) {
// Create forwarded conversations.
foreach ($to_array as $recipient_email) {
$forwarded_conversation = $conversation->replicate();
$forwarded_conversation->type = Conversation::TYPE_EMAIL;
$forwarded_conversation->setPreview($thread->body);
$forwarded_conversation->created_by_user_id = auth()->user()->id;
$forwarded_conversation->source_via = Conversation::PERSON_USER;
$forwarded_conversation->source_type = Conversation::SOURCE_TYPE_WEB;
$forwarded_conversation->threads_count = 0; // Counter will be incremented in ThreadObserver.
$forwarded_customer = Customer::create($recipient_email);
$forwarded_conversation->customer_id = $forwarded_customer->id;
// Reload customer object, otherwise it stores previous customer.
$forwarded_conversation->load('customer');
$forwarded_conversation->customer_email = $recipient_email;
$forwarded_conversation->subject = 'Fwd: '.$forwarded_conversation->subject;
//$forwarded_conversation->setCc(array_merge(Conversation::sanitizeEmails($request->cc), [$to]));
$forwarded_conversation->setCc(Conversation::sanitizeEmails($request->cc));
$forwarded_conversation->setBcc($request->bcc);
$forwarded_conversation->last_reply_at = $now;
$forwarded_conversation->last_reply_from = Conversation::PERSON_USER;
$forwarded_conversation->user_updated_at = $now;
if ($attachments_info['has_attachments']) {
$forwarded_conversation->has_attachments = true;
}
$forwarded_conversation->updateFolder();
$forwarded_conversation->save();
$forwarded_thread = $thread->replicate();
$forwarded_conversations[] = $forwarded_conversation;
$forwarded_threads[] = $forwarded_thread;
}
// Set forwarding meta data.
// todo: store array of numbers and IDs.
$thread->subtype = Thread::SUBTYPE_FORWARD;
$thread->setMeta('forward_child_conversation_number', $forwarded_conversation->number);
$thread->setMeta('forward_child_conversation_id', $forwarded_conversation->id);
}
// Conversation history.
if (!empty($request->conv_history)) {
if ($request->conv_history != 'global') {
if ($is_forward && !empty($forwarded_threads)) {
foreach ($forwarded_threads as $forwarded_thread) {
$forwarded_thread->setMeta(Thread::META_CONVERSATION_HISTORY, $request->conv_history);
}
} else {
$thread->setMeta(Thread::META_CONVERSATION_HISTORY, $request->conv_history);
}
}
}
\Eventy::action('thread.before_save_from_request', $thread, $request);
$thread->save();
// Save forwarded thread.
if ($is_forward) {
$forwarded_thread->conversation_id = $forwarded_conversation->id;
$forwarded_thread->type = Thread::TYPE_MESSAGE;
$forwarded_thread->subtype = null;
if ($attachments_info['has_attachments']) {
$forwarded_thread->has_attachments = true;
foreach ($forwarded_conversations as $i => $forwarded_conversation) {
$forwarded_thread = $forwarded_threads[$i];
$forwarded_thread->conversation_id = $forwarded_conversation->id;
$forwarded_thread->type = Thread::TYPE_MESSAGE;
$forwarded_thread->subtype = null;
if ($attachments_info['has_attachments']) {
$forwarded_thread->has_attachments = true;
}
$forwarded_thread->setMeta('forward_parent_conversation_number', $conversation->number);
$forwarded_thread->setMeta('forward_parent_conversation_id', $conversation->id);
$forwarded_thread->setMeta('forward_parent_thread_id', $thread->id);
$forwarded_thread->save();
}
$forwarded_thread->setMeta('forward_parent_conversation_number', $conversation->number);
$forwarded_thread->setMeta('forward_parent_conversation_id', $conversation->id);
$forwarded_thread->setMeta('forward_parent_thread_id', $thread->id);
$forwarded_thread->save();
}
// If thread has been created from draft, remove the draft
@@ -824,11 +897,22 @@ class ConversationsController extends Controller
// Set thread_id for uploaded attachments
if ($attachments_info['attachments']) {
if ($is_forward) {
$attachment_thread_id = $forwarded_thread->id;
// Copy attachments for each thread.
if (count($forwarded_threads) > 1) {
$attachments = Attachment::whereIn('id', $attachments_info['attachments'])->get();
}
foreach ($forwarded_threads as $i => $forwarded_thread) {
if ($i == 0) {
Attachment::whereIn('id', $attachments_info['attachments'])->update(['thread_id' => $forwarded_thread->id]);
} else {
foreach ($attachments as $attachment) {
$attachment->duplicate($forwarded_thread->id);
}
}
}
} else {
$attachment_thread_id = $thread->id;
Attachment::whereIn('id', $attachments_info['attachments'])->update(['thread_id' => $thread->id]);
}
Attachment::whereIn('id', $attachments_info['attachments'])->update(['thread_id' => $attachment_thread_id]);
}
// When user creates a new conversation it may be saved as draft first.
@@ -842,11 +926,15 @@ class ConversationsController extends Controller
// Forward.
// Notifications to users not sent.
event(new UserAddedNote($conversation, $thread));
// To send email with forwarded conversation.
event(new UserReplied($forwarded_conversation, $forwarded_thread));
\Eventy::action('conversation.user_forwarded_can_undo', $conversation, $thread, $forwarded_conversation, $forwarded_thread);
// After Conversation::UNDO_TIMOUT period trigger final event.
\Helper::backgroundAction('conversation.user_forwarded', [$conversation, $thread, $forwarded_conversation, $forwarded_thread], now()->addSeconds(Conversation::UNDO_TIMOUT));
foreach ($forwarded_conversations as $i => $forwarded_conversation) {
$forwarded_thread = $forwarded_threads[$i];
// To send email with forwarded conversation.
event(new UserReplied($forwarded_conversation, $forwarded_thread));
\Eventy::action('conversation.user_forwarded_can_undo', $conversation, $thread, $forwarded_conversation, $forwarded_thread);
// After Conversation::UNDO_TIMOUT period trigger final event.
\Helper::backgroundAction('conversation.user_forwarded', [$conversation, $thread, $forwarded_conversation, $forwarded_thread], now()->addSeconds(Conversation::UNDO_TIMOUT));
}
} elseif ($is_note) {
// Note.
event(new UserAddedNote($conversation, $thread));
@@ -887,14 +975,26 @@ class ConversationsController extends Controller
// Save conversation.
$conversation_copy->threads_count = 0;
$conversation_copy->customer_id = $customer_tmp->id;
// Reload customer, otherwise all recipients will have the same name.
$conversation_copy->load('customer');
$conversation_copy->customer_email = $customer_email;
$conversation_copy->has_attachments = $conversation->has_attachments;
$conversation_copy->push();
$thread_copy->conversation_id = $conversation_copy->id;
$thread_copy->customer_id = $customer_tmp->id;
$thread_copy->has_attachments = $conversation->has_attachments;
$thread_copy->setTo($customer_email);
$thread_copy->push();
// Copy attachments.
if (!empty($attachments_info['attachments'])) {
$attachments = Attachment::whereIn('id', $attachments_info['attachments'])->get();
foreach ($attachments as $attachment) {
$attachment->duplicate($thread_copy->id);
}
}
// Events.
// todo: allow to undo all emails
event(new UserCreatedConversation($conversation_copy, $thread_copy));
@@ -910,34 +1010,32 @@ class ConversationsController extends Controller
$show_view_link = false;
}
$flash_vars = ['%tag_start%' => '<strong>', '%tag_end%' => '</strong>', '%view_start%' => '&nbsp;<a href="'.$conversation->url().'">', '%a_end%' => '</a>&nbsp;', '%undo_start%' => '&nbsp;<a href="'.route('conversations.undo', ['thread_id' => $thread->id]).'" class="text-danger">'];
if ($is_phone) {
$flash_type = 'warning';
if ($show_view_link) {
$flash_text = ':%tag_start%Conversation created:%tag_end% :%view_start%View:%a_end% or :%undo_start%Undo:%a_end%';
$flash_text = __(':%tag_start%Conversation created:%tag_end% :%view_start%View:%a_end% or :%undo_start%Undo:%a_end%', $flash_vars);
} else {
$flash_text = '<strong>'.__('Conversation created').'</strong>';
}
} elseif ($is_note) {
$flash_type = 'warning';
if ($show_view_link) {
$flash_text = ':%tag_start%Note added:%tag_end% :%view_start%View:%a_end%';
$flash_text = __(':%tag_start%Note added:%tag_end% :%view_start%View:%a_end%', $flash_vars);
} else {
$flash_text = '<strong>'.__('Note added').'</strong>';
}
} else {
$flash_type = 'success';
if ($show_view_link) {
$flash_text = ':%tag_start%Email Sent:%tag_end% :%view_start%View:%a_end% or :%undo_start%Undo:%a_end%';
$flash_text = __(':%tag_start%Email Sent:%tag_end% :%view_start%View:%a_end% or :%undo_start%Undo:%a_end%', $flash_vars);
} else {
$flash_text = ':%tag_start%Email Sent:%tag_end% :%undo_start%Undo:%a_end%';
$flash_text = __(':%tag_start%Email Sent:%tag_end% :%undo_start%Undo:%a_end%', $flash_vars);
}
}
$flash_message = __(
$flash_text,
['%tag_start%' => '<strong>', '%tag_end%' => '</strong>', '%view_start%' => '&nbsp;<a href="'.$conversation->url().'">', '%a_end%' => '</a>&nbsp;', '%undo_start%' => '&nbsp;<a href="'.route('conversations.undo', ['thread_id' => $thread->id]).'" class="text-danger">']
);
\Session::flash('flash_'.$flash_type.'_floating', $flash_message);
\Session::flash('flash_'.$flash_type.'_floating', $flash_text);
}
break;
@@ -1257,8 +1355,8 @@ class ConversationsController extends Controller
$response['data'] = [
'thread_id' => $thread->id,
'to' => $thread->getToFirst(),
'cc' => $thread->getCcString(),
'bcc' => $thread->getBccString(),
'cc' => $thread->getCcArray(),
'bcc' => $thread->getBccArray(),
'body' => $thread->body,
'is_forward' => (int)$thread->isForward(),
'attachments' => $attachments,
@@ -1267,7 +1365,8 @@ class ConversationsController extends Controller
}
break;
// Load attachments from all threads in conversation.
// Load attachments from all threads in conversation
// when forwarding or creating a new conversation.
case 'load_attachments':
$conversation = Conversation::find($request->conversation_id);
if (!$conversation) {
@@ -1286,11 +1385,17 @@ class ConversationsController extends Controller
foreach ($conversation->threads as $thread) {
if ($thread->has_attachments) {
foreach ($thread->attachments as $attachment) {
if ($request->is_forwarding == 'true') {
$attachment_copy = $attachment->duplicate($thread->id);
} else {
$attachment_copy = $attachment;
}
$attachments[] = [
'id' => $attachment->id,
'name' => $attachment->file_name,
'size' => $attachment->size,
'url' => $attachment->url(),
'id' => $attachment_copy->id,
'name' => $attachment_copy->file_name,
'size' => $attachment_copy->size,
'url' => $attachment_copy->url(),
];
}
}
@@ -1313,14 +1418,14 @@ class ConversationsController extends Controller
$response['msg'] .= __('Action not authorized');
}
if (!$response['msg']) {
$mailbox_user = $user->mailboxes()->where('mailbox_id', $request->mailbox_id)->first();
$mailbox_user = $user->mailboxesWithSettings()->where('mailbox_id', $request->mailbox_id)->first();
if (!$mailbox_user) {
// Admin may not be connected to the mailbox yet
$user->mailboxes()->attach($request->mailbox_id);
// $mailbox_user = new MailboxUser();
// $mailbox_user->mailbox_id = $mailbox->id;
// $mailbox_user->user_id = $user->id;
$mailbox_user = $user->mailboxes()->where('mailbox_id', $request->mailbox_id)->first();
$mailbox_user = $user->mailboxesWithSettings()->where('mailbox_id', $request->mailbox_id)->first();
}
$mailbox_user->settings->after_send = $request->value;
$mailbox_user->settings->save();
@@ -1371,9 +1476,9 @@ class ConversationsController extends Controller
if (!$response['msg']) {
if ($request->sub_action == 'star') {
$conversation->addToFolder(Folder::TYPE_STARRED);
$conversation->addToFolder(Folder::TYPE_STARRED, $user->id);
} else {
$conversation->removeFromFolder(Folder::TYPE_STARRED);
$conversation->removeFromFolder(Folder::TYPE_STARRED, $user->id);
}
Conversation::clearStarredByUserCache($user->id, $conversation->mailbox_id);
$conversation->mailbox->updateFoldersCounters(Folder::TYPE_STARRED);
@@ -1507,6 +1612,13 @@ class ConversationsController extends Controller
$response['body'] = $thread->getCleanBody();
if (strip_tags($response['body'])) {
// Update the preview for the conversation if needed.
$last_thread = $thread->conversation->getLastThread([Thread::TYPE_CUSTOMER, Thread::TYPE_MESSAGE, Thread::TYPE_NOTE]);
if ($last_thread && $last_thread->id == $thread->id) {
$thread->conversation->setPreview($thread->body);
$thread->conversation->save();
}
$thread->save();
$response['status'] = 'success';
@@ -1528,7 +1640,7 @@ class ConversationsController extends Controller
if (!$user->can('update', $conversation)) {
continue;
}
if (!$conversation->mailbox->userHasAccess($new_user_id)) {
if ((int) $new_user_id != -1 && !$conversation->mailbox->userHasAccess($new_user_id)) {
continue;
}
@@ -1683,14 +1795,56 @@ class ConversationsController extends Controller
}
if (!$response['msg']) {
$prev_folder_id = Conversation::getFolderParam();
$prev_mailbox_id = $conversation->mailbox_id;
$conversation->moveToMailbox($mailbox, $user);
// If user does not have access to the new mailbox,
// redirect to the previous mailbox.
if (!$mailbox->userHasAccess($user->id)) {
if (!empty($prev_folder_id)) {
$response['redirect_url'] = route('mailboxes.view.folder', ['id' => $prev_mailbox_id, 'folder_id' => $prev_folder_id]);
} else {
$response['redirect_url'] = route('mailboxes.view', ['id' => $prev_mailbox_id]);
}
}
$response['status'] = 'success';
\Session::flash('flash_success_floating', __('Conversation moved'));
}
break;
// Merge conversations
case 'conversation_merge':
$conversation = Conversation::find($request->conversation_id);
if (!$conversation) {
$response['msg'] = __('Conversation not found');
}
if (!$response['msg'] && !$user->can('view', $conversation)) {
$response['msg'] = __('Not enough permissions');
}
$merge_conversation = Conversation::find($request->merge_conversation_id);
if (!$merge_conversation) {
$response['msg'] = __('Conversation not found');
}
if (!$response['msg'] && !$user->can('view', $merge_conversation)) {
$response['msg'] = __('Not enough permissions');
}
if (!$response['msg']) {
$conversation->mergeConversations($merge_conversation, $user);
$response['status'] = 'success';
\Session::flash('flash_success_floating', __('Conversations merged'));
}
break;
// Follow conversation
case 'follow':
case 'unfollow':
@@ -1732,6 +1886,47 @@ class ConversationsController extends Controller
break;
case 'update_subject':
$conversation = Conversation::find($request->conversation_id);
if (!$conversation) {
$response['msg'] = __('Conversation not found');
}
if (!$response['msg'] && !$conversation->mailbox->userHasAccess($user->id)) {
$response['msg'] = __('Not enough permissions');
}
$subject = $request->value ?? '';
$subject = trim($subject);
if (!$response['msg'] && $subject) {
$conversation->subject = $subject;
$conversation->save();
$response['status'] = 'success';
}
break;
case 'merge_search':
$conversation = Conversation::where('number', $request->number)->first();
if (!$conversation) {
$response['msg'] = __('Conversation not found');
}
if (!$response['msg'] && !$user->can('view', $conversation)) {
$response['msg'] = __('Conversation not found');
}
if (!$response['msg']) {
$response['html'] = \View::make('conversations/partials/merge_search_result')->with([
'conversation' => $conversation
])->render();
$response['status'] = 'success';
}
break;
default:
$response['msg'] = 'Unknown action';
break;
@@ -1758,6 +1953,10 @@ class ConversationsController extends Controller
return $this->ajaxHtmlChangeCustomer();
case 'move_conv':
return $this->ajaxHtmlMoveConv();
case 'merge_conv':
return $this->ajaxHtmlMergeConv();
case 'default_redirect':
return $this->ajaxHtmlDefaultRedirect();
}
abort(404);
@@ -1891,6 +2090,73 @@ class ConversationsController extends Controller
'mailboxes' => $user->mailboxesCanView(),
]);
}
/**
* Merge conversations.
*/
public function ajaxHtmlMergeConv()
{
$conversation_id = Input::get('conversation_id');
if (!$conversation_id) {
abort(404);
}
$conversation = Conversation::find($conversation_id);
if (!$conversation) {
abort(404);
}
$user = auth()->user();
if (!$user->can('view', $conversation)) {
\Helper::denyAccess();
}
$prev_conversations = [];
if ($conversation->customer_id) {
$prev_conversations = $conversation->mailbox->conversations()
->where('customer_id', $conversation->customer_id)
->where('id', '<>', $conversation->id)
->where('status', '!=', Conversation::STATUS_SPAM)
->where('state', Conversation::STATE_PUBLISHED)
->orderBy('created_at', 'desc')
->paginate(500);
}
return view('conversations/ajax_html/merge_conv', [
'conversation' => $conversation,
'prev_conversations' => $prev_conversations,
]);
}
/**
* Change default redirect for the mailbox.
*/
public function ajaxHtmlDefaultRedirect()
{
$mailbox_id = Input::get('mailbox_id');
if (!$mailbox_id) {
abort(404);
}
$mailbox = Mailbox::find($mailbox_id);
if (!$mailbox) {
abort(404);
}
$user = auth()->user();
if (!$user->can('view', $mailbox)) {
abort(403);
}
return view('conversations/ajax_html/default_redirect', [
'after_send' => $user->mailboxSettings($mailbox_id)->after_send,
'mailbox_id' => $mailbox_id,
]);
}
/**
* Get redirect URL after performing an action.
@@ -1905,6 +2171,7 @@ class ConversationsController extends Controller
if (!empty($request->after_send)) {
$after_send = $request->after_send;
} else {
// todo: use $user->mailboxSettings()
$after_send = $conversation->mailbox->getUserSettings($user->id)->after_send;
}
if (!empty($after_send)) {
@@ -1914,7 +2181,11 @@ class ConversationsController extends Controller
$redirect_url = $conversation->url();
break;
case MailboxUser::AFTER_SEND_FOLDER:
$redirect_url = route('mailboxes.view.folder', ['id' => $conversation->mailbox_id, 'folder_id' => $conversation->folder_id]);
$folder_id = Conversation::getFolderParam();
if (!$folder_id) {
$folder_id = $conversation->folder_id;
}
$redirect_url = route('mailboxes.view.folder', ['id' => $conversation->mailbox_id, 'folder_id' => $folder_id]);
break;
case MailboxUser::AFTER_SEND_NEXT:
$redirect_url = $conversation->urlNext(Conversation::getFolderParam());
@@ -1983,20 +2254,12 @@ class ConversationsController extends Controller
*/
public function ajaxConversationsPagination(Request $request, $response, $user)
{
$mailbox = Mailbox::find($request->mailbox_id);
//$mailbox = Mailbox::find($request->mailbox_id);
$folder = null;
$conversations = [];
if (!$mailbox) {
$response['msg'] = __('Mailbox not found');
}
if (!$response['msg'] && !$user->can('view', $mailbox)) {
$response['msg'] = __('Not enough permissions');
}
if (!$response['msg']) {
$folder = Folder::find($request->folder_id);
$folder = \Eventy::filter('conversations.ajax_pagination_folder', Folder::find($request->folder_id), $request, $response, $user);
if (!$folder) {
$response['msg'] = __('Folder not found');
}
@@ -2005,6 +2268,11 @@ class ConversationsController extends Controller
$response['msg'] = __('Not enough permissions');
}
// We should not use mailbox_id from the request, as it can be changed.
if (!$response['msg'] && !$user->can('view', $folder->mailbox)) {
$response['msg'] = __('Not enough permissions');
}
if (!$response['msg']) {
$query_conversations = Conversation::getQueryByFolder($folder, $user->id);
$conversations = $folder->queryAddOrderBy($query_conversations)->paginate(Conversation::DEFAULT_LIST_SIZE, ['*'], 'page', $request->page);
@@ -2052,21 +2320,27 @@ class ConversationsController extends Controller
session()->put('recent_search_queries', $recent_search_queries);
}
$conversations = $this->searchQuery($request, $user, $q, $filters);
$conversations = [];
if (\Eventy::filter('search.is_needed', true, 'conversations')) {
$conversations = $this->searchQuery($user, $q, $filters);
}
$customers = $this->searchCustomers($request, $user);
// Dummy folder
$folder = $this->getSearchFolder($conversations);
// List of available filters.
$filters_list = \Eventy::filter('search.filters_list', Conversation::$search_filters, $mode, $filters, $q);
//$filters_list_all = \Eventy::filter('search.filters_list_all', array_merge(Conversation::$search_filters, Customer::$search_filters), $filters, $q);
if ($mode == Conversation::SEARCH_MODE_CONV) {
$filters_list = \Eventy::filter('search.filters_list', Conversation::$search_filters, $mode, $filters, $q);
} else {
$filters_list = \Eventy::filter('search.filters_list_customers', Customer::$search_filters, $mode, $filters, $q);
}
$mailboxes = \Cache::remember('search_filter_mailboxes_'.$user->id, 5, function () use ($user) {
return $user->mailboxesCanView();
});
$users = \Cache::remember('search_filter_users_'.$user->id, 5, function () use ($user, $mailboxes) {
return $user->whichUsersCanView($mailboxes);
return \Eventy::filter('search.assignees', $user->whichUsersCanView($mailboxes), $user, $mailboxes);
});
return view('conversations/search', [
@@ -2100,103 +2374,9 @@ class ConversationsController extends Controller
/**
* Search conversations.
*/
public function searchQuery($request, $user, $q, $filters)
public function searchQuery($user, $q, $filters)
{
// Get IDs of mailboxes to which user has access
$mailbox_ids = $user->mailboxesIdsCanView();
// Filters
//$filters = $request->f ?? [];
// Search query
//$q = $this->getSearchQuery($request);
// Like is case insensitive.
$like = '%'.mb_strtolower($q).'%';
$query_conversations = Conversation::select('conversations.*')
// https://github.com/laravel/framework/issues/21242
// https://github.com/laravel/framework/pull/27675
->groupby('conversations.id')
->whereIn('conversations.mailbox_id', $mailbox_ids)
->join('threads', function ($join) {
$join->on('conversations.id', '=', 'threads.conversation_id');
});
if ($q) {
$query_conversations->where(function ($query) use ($like, $filters, $q) {
$query->where('conversations.subject', 'like', $like)
->orWhere('conversations.customer_email', 'like', $like)
->orWhere('conversations.number', 'like', $like)
->orWhere('conversations.id', 'like', $like)
->orWhere('threads.body', 'like', $like)
->orWhere('threads.from', 'like', $like)
->orWhere('threads.to', 'like', $like)
->orWhere('threads.cc', 'like', $like)
->orWhere('threads.bcc', 'like', $like);
$query = \Eventy::filter('search.conversations.or_where', $query, $filters, $q);
});
}
// Apply search filters.
if (!empty($filters['assigned'])) {
$query_conversations->where('conversations.user_id', $filters['assigned']);
}
if (!empty($filters['customer'])) {
$customer_id = $filters['customer'];
$query_conversations->where(function ($query) use ($customer_id) {
$query->where('conversations.customer_id', '=', $customer_id)
->orWhere('threads.created_by_customer_id', '=', $customer_id);
});
}
if (!empty($filters['mailbox'])) {
$query_conversations->where('conversations.mailbox_id', '=', $filters['mailbox']);
}
if (!empty($filters['status'])) {
if (count($filters['status']) == 1) {
// = is faster than IN.
$query_conversations->where('conversations.status', '=', $filters['status'][0]);
} else {
$query_conversations->whereIn('conversations.status', $filters['status']);
}
}
if (!empty($filters['subject'])) {
$query_conversations->where('conversations.subject', 'like', '%'.mb_strtolower($filters['subject']).'%');
}
if (!empty($filters['attachments'])) {
$has_attachments = ($filters['attachments'] == 'yes' ? true : false);
$query_conversations->where('conversations.has_attachments', '=', $has_attachments);
}
if (!empty($filters['type'])) {
$query_conversations->where('conversations.has_attachments', '=', $filters['type']);
}
if (!empty($filters['body'])) {
$query_conversations->where('threads.body', 'like', '%'.mb_strtolower($filters['body']).'%');
}
if (!empty($filters['number'])) {
$query_conversations->where('conversations.number', '=', $filters['number']);
}
if (!empty($filters['following'])) {
if ($filters['following'] == 'yes') {
$query_conversations->join('followers', function ($join) {
$join->on('followers.conversation_id', '=', 'conversations.id');
$join->where('followers.user_id', auth()->user()->id);
});
}
}
if (!empty($filters['id'])) {
$query_conversations->where('conversations.id', '=', $filters['id']);
}
if (!empty($filters['after'])) {
$query_conversations->where('conversations.created_at', '>=', date('Y-m-d 00:00:00', strtotime($filters['after'])));
}
if (!empty($filters['before'])) {
$query_conversations->where('conversations.created_at', '<=', date('Y-m-d 23:59:59', strtotime($filters['before'])));
}
$query_conversations = \Eventy::filter('search.conversations.apply_filters', $query_conversations, $filters, $q);
$query_conversations->orderBy('conversations.last_reply_at');
$query_conversations = Conversation::search($q, $filters, $user);
return $query_conversations->paginate(Conversation::DEFAULT_LIST_SIZE);
}
@@ -2260,7 +2440,9 @@ class ConversationsController extends Controller
// Like is case insensitive.
$like = '%'.mb_strtolower($q).'%';
$query_customers = Customer::select(['customers.*', 'emails.email'])
// We need to use aggregate function for email to avoid "Grouping error" error in PostgreSQL.
$query_customers = Customer::select(['customers.*', \DB::raw('MAX(emails.email)')])
->groupby('customers.id')
->leftJoin('emails', function ($join) {
$join->on('customers.id', '=', 'emails.customer_id');
})
@@ -2272,7 +2454,6 @@ class ConversationsController extends Controller
->orWhere('customers.phones', 'like', $like)
->orWhere('customers.websites', 'like', $like)
->orWhere('customers.social_profiles', 'like', $like)
->orWhere('customers.chats', 'like', $like)
->orWhere('customers.address', 'like', $like)
->orWhere('customers.city', 'like', $like)
->orWhere('customers.state', 'like', $like)
@@ -2281,6 +2462,14 @@ class ConversationsController extends Controller
->orWhere('emails.email', 'like', $like);
});
if (!empty($filters['mailbox'])) {
$query_customers->join('conversations', function ($join) use ($filters) {
$join->on('conversations.customer_id', '=', 'customers.id');
//$join->on('conversations.mailbox_id', '=', $filters['mailbox']);
});
$query_customers->where('conversations.mailbox_id', '=', $filters['mailbox']);
}
$query_customers = \Eventy::filter('search.customers.apply_filters', $query_customers, $filters, $q);
return $query_customers->paginate(50);
@@ -2306,7 +2495,7 @@ class ConversationsController extends Controller
{
if (array_key_exists('q', $request->filter)) {
// Search
$conversations = $this->searchQuery($request, $user, $this->getSearchQuery($request), $this->getSearchFilters($request));
$conversations = $this->searchQuery($user, $this->getSearchQuery($request), $this->getSearchFilters($request));
} else {
// Filters
$conversations = $this->conversationsFilterQuery($request, $user);
@@ -2316,6 +2505,7 @@ class ConversationsController extends Controller
$response['html'] = view('conversations/conversations_table', [
'conversations' => $conversations,
'params' => $request->params ?? [],
])->render();
return $response;
@@ -2447,9 +2637,9 @@ class ConversationsController extends Controller
$customer = null;
// Check to prevent creating empty customers.
if (trim($request->name) || trim($request->phone)) {
$request->name = trim($request->name);
$request->phone = trim($request->phone);
if (trim($request->name ?? '') || trim($request->phone ?? '')) {
$request->name = trim($request->name ?? '');
$request->phone = trim($request->phone ?? '');
$name_parts = explode(' ', $request->name);
$customer_data['first_name'] = $name_parts[0];
@@ -2473,6 +2663,14 @@ class ConversationsController extends Controller
}
}
// Try to find customer by phone.
if (!$customer && $request->phone) {
$customer = Customer::findByPhone($request->phone);
if ($customer) {
$customer_email = $customer->getMainEmail();
}
}
if (!$customer) {
// Create customer with passed name, email and phone
if (Email::sanitizeEmail($request->to_email)) {

View File

@@ -67,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)
@@ -128,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);
@@ -158,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]);
}
@@ -206,6 +230,7 @@ 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);
@@ -358,6 +383,18 @@ class CustomersController extends Controller
}
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;

403
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('mailbox.settings_before_save', $mailbox, $request);
$mailbox->fill($request->all());
$mailbox->save();
@@ -151,12 +222,25 @@ class MailboxesController extends Controller
{
$mailbox = Mailbox::findOrFail($id);
$this->authorize('update', $mailbox);
$this->authorize('updatePermissions', $mailbox);
$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,
]);
}
/**
@@ -168,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]);
@@ -184,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)]);
}
@@ -195,7 +315,7 @@ 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(), [
@@ -228,7 +348,7 @@ class MailboxesController extends Controller
// 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]);
@@ -237,16 +357,16 @@ 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);
$fields = [
'in_server' => $mailbox->in_server,
'in_port' => $mailbox->in_port,
'in_username' => $mailbox->in_username,
'in_password' => $mailbox->in_password,
'in_server' => $mailbox->in_server ?? '',
'in_port' => $mailbox->in_port ?? '',
'in_username' => $mailbox->in_username ?? '',
'in_password' => $mailbox->in_password ?? '',
];
$validator = Validator::make($fields, [
@@ -265,7 +385,7 @@ 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' => 'nullable|string|max:255',
@@ -286,19 +406,21 @@ class MailboxesController extends Controller
]);
// Do not save dummy password.
if (preg_match("/^\*+$/", $request->in_password)) {
if (preg_match("/^\*+$/", $request->in_password ?? '')) {
$params = $request->except(['in_password']);
} else {
$params = $request->all();
}
\Eventy::action('mailbox.incoming_settings_before_save', $mailbox, $request);
$mailbox->fill($params);
// Save IMAP Folders.
// Save all custom folders except INBOX.
$in_imap_folders = [];
foreach ($request->in_imap_folders as $imap_folder) {
if (mb_strtolower($imap_folder) != 'inbox') {
if (is_array($request->in_imap_folders)) {
foreach ($request->in_imap_folders as $imap_folder) {
$in_imap_folders[] = $imap_folder;
}
}
@@ -316,10 +438,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();
@@ -338,8 +464,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);
@@ -355,7 +479,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',
@@ -381,7 +505,7 @@ 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%}';
@@ -399,7 +523,8 @@ 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),
@@ -433,6 +558,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.
*/
@@ -453,14 +596,14 @@ 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']) {
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>']);
@@ -477,7 +620,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.');
}
}
@@ -497,19 +640,23 @@ 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');
}
$response = \Eventy::filter('mailbox.fetch_test', $response, $mailbox);
$tested = (isset($response['tested']) && $response['tested'] === true);
// Check if outgoing port is open.
if (!$response['msg']) {
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']) {
if (!$response['msg'] && !$tested) {
$test_result = false;
try {
@@ -519,11 +666,11 @@ class MailboxesController extends Controller
}
if (!$test_result && !$response['msg']) {
$response['msg'] = __('Error occurend connecting to the server');
$response['msg'] = __('Error occurred connecting to the server');
}
}
if (!$response['msg']) {
if (!$response['msg'] && !$tested) {
$response['status'] = 'success';
}
break;
@@ -534,7 +681,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');
}
@@ -586,7 +733,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');
@@ -610,6 +757,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($mailbox->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;
@@ -621,4 +793,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,6 +28,8 @@ class ModulesController extends Controller
{
$installed_modules = [];
$modules_directory = [];
$third_party_modules = [];
$all_modules = [];
$flashes = [];
$updates_available = false;
@@ -53,8 +55,7 @@ class ModulesController extends Controller
\Module::clearCache();
$modules = \Module::all();
foreach ($modules as $module) {
$img = '';
$installed_modules[] = [
$module_data = [
'alias' => $module->getAlias(),
'name' => $module->getName(),
'description' => $module->getDescription(),
@@ -66,7 +67,7 @@ class ModulesController extends Controller
'requiredPhpExtensions' => $module->get('requiredPhpExtensions'),
'requiredPhpExtensionsMissing' => \App\Module::getMissingExtensions($module->get('requiredPhpExtensions')),
'requiredModulesMissing' => \App\Module::getMissingModules($module->get('requiredModules'), $modules),
'img' => $img,
'img' => $module->get('img'),
'active' => $module->active(), //\App\Module::isActive($module->getAlias()),
'installed' => true,
'activated' => \App\Module::isLicenseActivated($module->getAlias(), $module->get('authorUrl')),
@@ -74,6 +75,8 @@ class ModulesController extends Controller
// Determined later
'new_version' => '',
];
$module_data = \App\Module::formatModuleData($module_data);
$installed_modules[] = $module_data;
}
// No need, as we update modules list on each page load
@@ -85,10 +88,14 @@ class ModulesController extends Controller
// Prepare directory modules
if (is_array($modules_directory)) {
foreach ($modules_directory as $i_dir => $dir_module) {
$modules_directory[$i_dir] = \App\Module::formatModuleData($dir_module);
// Remove modules without aliases
if (empty($dir_module['alias'])) {
unset($modules_directory[$i_dir]);
}
$all_modules[$dir_module['alias']] = $dir_module['name'];
foreach ($installed_modules as $i_installed => $module) {
if ($dir_module['alias'] == $module['alias']) {
// Set image from director
@@ -116,6 +123,12 @@ class ModulesController extends Controller
}
$modules_directory[$i_dir]['active'] = \App\Module::isActive($dir_module['alias']);
$modules_directory[$i_dir]['activated'] = false;
// Do not show third-party modules in Modules Derectory.
if (\App\Module::isThirdParty($dir_module)) {
$third_party_modules[] = $modules_directory[$i_dir];
unset($modules_directory[$i_dir]);
}
}
} else {
$modules_directory = [];
@@ -124,8 +137,10 @@ class ModulesController extends Controller
return view('modules/modules', [
'installed_modules' => $installed_modules,
'modules_directory' => $modules_directory,
'third_party_modules' => $third_party_modules,
'flashes' => $flashes,
'updates_available' => $updates_available,
'all_modules' => $all_modules,
]);
}
@@ -279,6 +294,22 @@ class ModulesController extends Controller
$response['msg'] = __('Your domain is deactivated');
break;
}
} elseif (!empty($license_result['status']) && $license_result['status'] == 'inactive') {
// Activate the license.
$result = WpApi::activateLicense($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'] == 'valid') {
// Success.
} elseif (!empty($result['error'])) {
$response['msg'] = $this->getErrorMessage($result['error'], $result);
} else {
// Some unknown error. Do nothing.
}
}
}
}
}
@@ -308,7 +339,7 @@ class ModulesController extends Controller
$type = 'success';
$msg = __('":name" module successfully activated!', ['name' => $name]);
} else {
// Deactivate module
// Deactivate the module.
\App\Module::setActive($alias, false);
\Artisan::call('freescout:clear-cache');
}
@@ -324,6 +355,11 @@ class ModulesController extends Controller
}
}
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>',
@@ -380,7 +416,7 @@ class ModulesController extends Controller
$params = [
'license' => $license,
'module_alias' => $alias,
'url' => \App\Module::getAppUrl(),
'url' => (!empty($request->any_url) ? '*' : \App\Module::getAppUrl()),
];
$result = WpApi::deactivateLicense($params);
@@ -390,12 +426,15 @@ class ModulesController extends Controller
$response['msg'] = $result['message'];
} else {
if (!empty($result['status']) && $result['status'] == 'success') {
// Remove remembered license key and deactivate license in DB
\App\Module::deactivateLicense($alias, '');
$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', []);
// Deactivate module
\App\Module::setActive($alias, false);
\Artisan::call('freescout:clear-cache', []);
}
// Flash does not work here.
$flash = [
@@ -574,7 +613,7 @@ class ModulesController extends Controller
$msg = __('License key has been revoked');
break;
case 'no_activations_left':
$msg = __('No activations left for this license key');
$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');

View File

@@ -2,6 +2,7 @@
namespace App\Http\Controllers;
use App\Attachment;
use App\Conversation;
use App\Option;
use App\Thread;
@@ -33,6 +34,10 @@ 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]);
}
@@ -96,7 +101,8 @@ class PublicController extends Controller
$user->invite_state = User::INVITE_STATE_ACTIVATED;
$user->invite_hash = '';
$user = \Eventy::filter('user.setup_save', $user, $request);
$user->save();
// Login user
@@ -123,7 +129,7 @@ class PublicController extends Controller
}
// 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%', 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);
$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);
@@ -134,4 +140,75 @@ class PublicController extends Controller
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;
}
/**
* Needed for the mobile app.
*/
public function mobilePing()
{
echo file_get_contents(public_path('installer/css/fontawesome.css'));
}
}

View File

@@ -3,6 +3,7 @@
namespace App\Http\Controllers;
use App\ActivityLog;
use App\Misc\Helper;
use App\SendLog;
use App\Thread;
use App\User;
@@ -27,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]);
}
@@ -119,6 +128,9 @@ class SecureController extends Controller
$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[] = [
@@ -158,7 +170,7 @@ class SecureController extends Controller
$name = '';
if (!empty($request->name)) {
$activities = ActivityLog::inLog($request->name)->orderBy('created_at', 'desc')->get();
//$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;
@@ -177,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

@@ -4,6 +4,7 @@ namespace App\Http\Controllers;
use App\Conversation;
use App\Option;
use App\Subscription;
use App\User;
use Illuminate\Http\Request;
use Validator;
@@ -100,7 +101,8 @@ class SettingsController extends Controller
'env' => 'APP_FETCH_SCHEDULE',
],
'mail_password' => [
'safe_password' => true
'safe_password' => true,
'encrypt' => true,
]
],
];
@@ -108,19 +110,39 @@ class SettingsController extends Controller
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' => [
@@ -165,9 +187,11 @@ class SettingsController extends Controller
$settings = [
'company_name' => Option::get('company_name', \Config::get('app.name')),
'next_ticket' => (Option::get('next_ticket') >= Conversation::max('number') + 1) ? Option::get('next_ticket') : Conversation::max('number') + 1,
'user_permissions' => Option::get('user_permissions', []),
'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(),
@@ -181,7 +205,7 @@ class SettingsController extends Controller
'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_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'),
];
@@ -194,6 +218,7 @@ class SettingsController extends Controller
'alert_logs',
'alert_logs_names',
'alert_logs_period',
'subscription_defaults',
], [
'alert_logs_names' => [],
'alert_logs' => config('app.alert_logs'),
@@ -267,7 +292,21 @@ class SettingsController extends Controller
// 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;
@@ -276,6 +315,11 @@ class SettingsController extends Controller
// 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 {
// If option does not exist, default will be used,
@@ -300,9 +344,14 @@ class SettingsController extends Controller
\Helper::clearCache(['--doNotGenerateVars' => true]);
}
\Session::flash('flash_success_floating', __('Settings updated'));
// \Helper::clearCache prevents \Session::flash() from displaying.
$request->session()->flash('flash_success_floating', __('Settings updated'));
return redirect()->route('settings', ['section' => $section]);
$response = redirect()->route('settings', ['section' => $section]);
$response = \Eventy::filter('settings.after_save', $response, $request, $section, $settings);
return $response;
}
/**
@@ -336,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

@@ -19,7 +19,9 @@ class SystemController extends Controller
*/
public function __construct()
{
$this->middleware('auth');
$this->middleware('auth', ['except' => [
'cron'
]]);
}
/**
@@ -43,6 +45,13 @@ 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) {
@@ -56,6 +65,18 @@ class SystemController extends Controller
'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');
@@ -79,7 +100,7 @@ class SystemController extends Controller
// Commands
$commands_list = [
'freescout:fetch-emails' => 'freescout:fetch-emails',
'freescout:fetch-emails' => 'freescout:fetch-emails',
\Helper::getWorkerIdentifier() => 'queue:work'
];
foreach ($commands_list as $command_identifier => $command_name) {
@@ -168,19 +189,23 @@ class SystemController extends Controller
];
}
// Check new version
// Check new version if enabled
$new_version_available = false;
$latest_version = \Cache::remember('latest_version', 15, function () {
try {
return \Updater::getVersionAvailable();
} catch (\Exception $e) {
SystemController::$latest_version_error = $e->getMessage();
return '';
}
});
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;
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', [
@@ -189,18 +214,25 @@ class SystemController extends Controller
'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'));
@@ -259,6 +291,10 @@ class SystemController extends Controller
case 'migrate_db':
\Artisan::call('migrate', ['--force' => true], $outputLog);
break;
case 'logout_users':
\Artisan::call('freescout:logout-users', [], $outputLog);
break;
}
$output = $outputLog->fetch();
@@ -287,7 +323,7 @@ class SystemController extends Controller
case 'update':
try {
$status = \Updater::update();
// Artisan::output()
} catch (\Exception $e) {
$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>']);
@@ -303,14 +339,18 @@ class SystemController extends Controller
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;
@@ -326,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,6 +2,7 @@
namespace App\Http\Controllers;
use \Barryvdh\TranslationManager\Models\Translation;
use Barryvdh\TranslationManager\Controller as BaseController;
class TranslateController extends BaseController
@@ -15,6 +16,13 @@ class TranslateController extends BaseController
{
$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
@@ -31,7 +39,7 @@ class TranslateController extends BaseController
$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) {

View File

@@ -57,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();
@@ -79,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);
@@ -86,6 +98,7 @@ class UsersController extends Controller
// Set some random password before sending invite
$user->password = Hash::make($user->generateRandomPassword());
}
$user = \Eventy::filter('user.create_save', $user, $request);
$user->save();
$user->mailboxes()->sync($request->mailboxes);
@@ -141,6 +154,8 @@ class UsersController extends Controller
*/
public function profileSave($id, Request $request)
{
$invalid = false;
$user = User::findOrFail($id);
$this->authorize('update', $user);
@@ -149,7 +164,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',
@@ -169,6 +184,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.'));
}
}
@@ -177,12 +193,18 @@ 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();
@@ -201,12 +223,21 @@ class UsersController extends Controller
if (!auth()->user()->can('changeRole', $user)) {
unset($request_data['role']);
}
$user->fill($request_data);
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->setData($request_data);
if (empty($request->input('enable_kb_shortcuts'))) {
$user->enable_kb_shortcuts = false;
}
$user = \Eventy::filter('user.save_profile', $user, $request);
$user->save();
\Session::flash('flash_success_floating', __('Profile saved successfully'));
@@ -256,6 +287,21 @@ class UsersController extends Controller
$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]);
@@ -512,7 +558,7 @@ class UsersController extends Controller
$user->status = \App\User::STATUS_DELETED;
// Update email.
$email_suffix = '_deleted'.date('YmdHis');
$email_suffix = User::EMAIL_DELETED_SUFFIX.date('YmdHis');
// We have to truncate email to avoid "Data too long" error.
$user->email = mb_substr($user->email, 0, User::EMAIL_MAX_LENGTH - mb_strlen($email_suffix)).$email_suffix;

View File

@@ -19,7 +19,6 @@ class Kernel extends HttpKernel
\App\Http\Middleware\TrimStrings::class,
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
\App\Http\Middleware\TrustProxies::class,
// todo: These two may need to be moved into 'web' not to be called in 'api'.
\App\Http\Middleware\ResponseHeaders::class,
\App\Http\Middleware\TerminateHandler::class,
];
@@ -45,10 +44,10 @@ class Kernel extends HttpKernel
\App\Http\Middleware\CustomHandle::class,
],
'api' => [
'throttle:60,1',
'bindings',
],
// 'api' => [
// 'throttle:60,1',
// 'bindings',
// ],
];
/**

View File

@@ -19,6 +19,6 @@ class CustomHandle
// Hook.
\Eventy::action('middleware.web.custom_handle', $request);
return $next($request);
return \Eventy::filter('middleware.web.custom_handle.response', $next($request), $request, $next);
}
}

View File

@@ -38,6 +38,14 @@ class HttpsRedirect {
return redirect()->secure($request->getRequestUri());
}
}
// Correct protocol in $_SERVER
if (parse_url(config('app.url'), PHP_URL_SCHEME) == 'https'
&& !$request->secure()
&& strtolower($_SERVER['HTTPS'] ?? '') != 'on'
) {
$_SERVER['HTTPS'] = 'on';
}
return $next($request);
}
}

View File

@@ -19,12 +19,9 @@ class Localize
// Interface language is set automatically, as locale is stored in .env file.
// Set user language if user logged in.
$user_locale = session('user_locale');
$user_locale = \Eventy::filter('locale', session('user_locale'));
if ($user_locale) {
// app()->setLocale() also dynamically changes config('app.locale'),
// so we have to remember current locale in real_locale parameter.
\Config::set('app.real_locale', config('app.locale'));
app()->setLocale($user_locale);
\Helper::setLocale($user_locale);
}
return $next($request);

View File

@@ -20,7 +20,7 @@ class LogoutIfDeleted
public function handle($request, Closure $next, $guard = null)
{
$user = Auth::user();
if ($user && $user->status == \App\User::STATUS_DELETED) {
if ($user && ($user->isDeleted() || $user->isDisabled())) {
Auth::logout();
return Redirect::route('login');
}

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

@@ -44,7 +44,7 @@ class SendAutoReply implements ShouldQueue
public function handle()
{
// Configure mail driver according to Mailbox settings
\App\Misc\Mail::setMailDriver($this->mailbox);
\App\Misc\Mail::setMailDriver($this->mailbox, null, $this->conversation);
// Auto reply appears as reply in customer's mailbox
$headers['In-Reply-To'] = '<'.$this->thread->message_id.'>';
@@ -55,6 +55,11 @@ 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;

View File

@@ -47,21 +47,34 @@ class SendNotificationToUsers implements ShouldQueue
$mailbox = $this->conversation->mailbox;
// Configure mail driver according to Mailbox settings
\App\Misc\Mail::setMailDriver($mailbox);
\App\Misc\Mail::setMailDriver($mailbox, null, $this->conversation);
// Threads has to be sorted here, if sorted before, they come here in wrong order
$this->threads = $this->threads->sortByDesc(function ($item, $key) {
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.'>';
@@ -72,6 +85,15 @@ class SendNotificationToUsers implements ShouldQueue
foreach ($this->users as $user) {
// User can ne deleted from DB.
if (!isset($user->id)) {
continue;
}
if ($user->isDeleted()) {
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) {
@@ -92,7 +114,10 @@ class SendNotificationToUsers implements ShouldQueue
// 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').' '.$mailbox->name;
}
@@ -105,6 +130,9 @@ class SendNotificationToUsers implements ShouldQueue
// Set user language
app()->setLocale($user->getLocale());
$headers['X-FreeScout-Mail-Type'] = 'user.notification';
$headers = \Eventy::filter('jobs.send_reply_to_customer.headers', $headers, $user, $mailbox, $this->conversation, $this->threads, $from);
$exception = null;
try {
@@ -146,7 +174,13 @@ class SendNotificationToUsers implements ShouldQueue
// 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) {
$this->release(3600);
if ($this->attempts() == 1) {
// Second attempt after 5 min.
$this->release(300);
} else {
// Others - after 1 hour.
$this->release(3600);
}
throw $global_exception;
} else {

View File

@@ -11,6 +11,7 @@ use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Mail;
use Webklex\IMAP\Client;
class SendReplyToCustomer implements ShouldQueue
{
@@ -51,6 +52,7 @@ 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) {
@@ -59,11 +61,16 @@ class SendReplyToCustomer implements ShouldQueue
$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) {
@@ -79,19 +86,22 @@ class SendReplyToCustomer implements ShouldQueue
}
}
$this->threads = $this->threads->merge($forwarded_replies);
$send_previous_messages = true;
$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();
if ($this->last_thread === null) {
return;
}
$last_customer_thread = null;
// If thread is draft, it means it has been undone
@@ -113,13 +123,46 @@ class SendReplyToCustomer implements ShouldQueue
}
}
// 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);
\App\Misc\Mail::setMailDriver($mailbox, $this->last_thread->created_by_user, $this->conversation);
// Get penultimate email Message-Id if reply
if (!$new && !empty($last_customer_thread) && $last_customer_thread->message_id) {
@@ -127,7 +170,7 @@ class SendReplyToCustomer implements ShouldQueue
$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;
@@ -168,11 +211,15 @@ class SendReplyToCustomer implements ShouldQueue
// $bcc_array = [];
// }
$headers['X-FreeScout-Mail-Type'] = 'customer.message';
$reply_mail = new ReplyToCustomer($this->conversation, $this->threads, $headers, $mailbox);
try {
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
if ($this->attempts() == 1) {
@@ -194,15 +241,23 @@ class SendReplyToCustomer implements ShouldQueue
$this->saveToSendLog($e->getMessage());
}
$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) {
$this->release(3600);
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' => $e->getMessage()]);
$this->last_thread->updateSendStatusData(['msg' => $error_message]);
$this->last_thread->save();
// This executes $this->failed().
@@ -212,6 +267,53 @@ class SendReplyToCustomer implements ShouldQueue
}
}
$imap_sent_folder = $mailbox->imap_sent_folder;
if ($imap_sent_folder) {
try {
$client = \MailHelper::getMailboxClient($mailbox);
$client->connect();
$envelope['from'] = $mailbox->getMailFrom(null, $this->conversation)['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 {
// getFolder does not work if sent folder has spaces.
$folder = $client->getFolder($imap_sent_folder);
if ($folder) {
if (get_class($client) == 'Webklex\PHPIMAP\Client') {
$folder->appendMessage(imap_mail_compose($envelope, [$part1]), ['Seen'], now()->format('d-M-Y H:i:s O'));
} else {
$folder->appendMessage(imap_mail_compose($envelope, [$part1]), '\Seen', now()->format('d-M-Y H:i:s O'));
}
} else {
\Log::error('Could not save outgoing reply to the IMAP folder (make sure IMAP folder does not have spaces - folders with spaces do not work): '.$imap_sent_folder);
}
} 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
// Outcoming message_id can be generated for each thread by thread->id
// $this->last_thread->message_id = $message_id;

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

@@ -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,7 @@ 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.
@@ -35,20 +39,52 @@ class SendAutoReply
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(10);
$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 >= 3) {
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
/*$created_at = \Illuminate\Support\Carbon::now()->subDays(1);

View File

@@ -62,11 +62,12 @@ class SendNotificationToUsers
if (empty($event->conversation) || !$event_type) {
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) {
// Ignore imported threads.
if (!empty($event->thread) && $event->thread->imported) {
return;
}
$conversation = $event->conversation;
// Using the last argument you can make event to be processed immediately
Subscription::registerEvent($event_type, $conversation, $caused_by_user_id/*, true*/);

View File

@@ -28,11 +28,26 @@ class SendReplyToCustomer
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

@@ -56,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

@@ -81,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

View File

@@ -23,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

@@ -27,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,6 +2,8 @@
namespace App;
use App\Email;
use App\User;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Hash;
use Watson\Rememberable\Rememberable;
@@ -11,7 +13,7 @@ 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.
*/
@@ -91,6 +93,28 @@ 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.
*/
@@ -104,20 +128,24 @@ 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', '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'];
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));
// });
}
/**
@@ -125,7 +153,11 @@ class Mailbox extends Model
*/
public function setInPasswordAttribute($value)
{
$this->attributes['in_password'] = encrypt($value);
if ($value != '') {
$this->attributes['in_password'] = encrypt($value);
} else {
$this->attributes['in_password'] = '';
}
}
/**
@@ -141,7 +173,36 @@ class Mailbox extends Model
return decrypt($value);
} catch (\Exception $e) {
// do nothing if decrypt wasn't succefull
return false;
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 '';
}
}
@@ -150,7 +211,16 @@ 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');
}
/**
@@ -214,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,
]);
}
}
}
@@ -255,6 +325,11 @@ 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])
@@ -297,7 +372,7 @@ class Mailbox extends Model
->orderBy('type')
->get();
return \Eventy::filter('mailbox.folders', $folders);
return \Eventy::filter('mailbox.folders', $folders, $this);
}
/**
@@ -333,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 {
@@ -363,22 +444,58 @@ class Mailbox extends Model
{
$admins = User::where('role', User::ROLE_ADMIN)->select($fields)->remember(\Helper::cacheTime($cache))->get();
$users = $this->users()->select($fields)->rememberForever()->get()->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->isDeleted()) {
if (!$user->isActive()) {
$users->forget($i);
}
}
// Sort by full name
if ($sort) {
$users = $users->sortBy(function ($value, $key) {
return $value->getFullName();
}, SORT_STRING | SORT_FLAG_CASE);
$users = User::sortUsers($users);
}
$users = \Eventy::filter('mailbox.users_having_access', $users, $this, $cache, $fields, $sort);
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);
$users = \Eventy::filter('mailbox.users_assignable', $users, $this, $cache);
return $users;
}
@@ -403,9 +520,16 @@ class Mailbox extends Model
public function userHasAccess($user_id, $user = null)
{
if (!$user) {
$user = User::find($user_id);
if ($user_id instanceof \App\User) {
$user = $user_id;
} else {
$user = User::find($user_id);
}
}
if ($user && $user->isAdmin()) {
$filter = \Eventy::filter('mailbox.user_has_access', -1, $this, $user);
if ($filter != -1) {
return (bool)$filter;
} elseif ($user && $user->isAdmin()) {
return true;
} else {
return (bool) $this->users()->where('users.id', $user_id)->count();
@@ -416,10 +540,11 @@ class Mailbox extends Model
* Get From array for the Mail function.
*
* @param App\User $from_user
* @param App\Conversation $conversation
*
* @return array
*/
public function getMailFrom($from_user = null)
public function getMailFrom($from_user = null, $conversation = null)
{
// Mailbox name by default
$name = $this->name;
@@ -430,7 +555,7 @@ class Mailbox extends Model
$name = $from_user->getFullName();
}
return ['address' => $this->email, 'name' => $name];
return [ 'address' => \Eventy::filter( 'mailbox.get_mail_from_address', $this->email, $from_user, $conversation ), 'name' => \Eventy::filter( 'mailbox.get_mail_from_name', $name, $from_user, $conversation ) ];
}
/**
@@ -490,7 +615,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);
}
/**
@@ -498,19 +648,40 @@ 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.
*
@@ -583,7 +754,7 @@ class Mailbox extends Model
*/
public function url()
{
return route('mailboxes.view', ['id' => $this->id]);
return \Eventy::filter('mailbox.url', route('mailboxes.view', ['id' => $this->id]), $this);
}
/**
@@ -615,7 +786,12 @@ class Mailbox extends Model
*/
public function getInImapFolders()
{
return \Helper::jsonToArray($this->in_imap_folders);
$in_imap_folders = \Helper::jsonToArray($this->in_imap_folders);
if (count($in_imap_folders)) {
return $in_imap_folders;
} else {
return ["INBOX"];
}
}
public function outPasswordSafe()
@@ -627,4 +803,112 @@ class Mailbox extends Model
{
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 getMeta($key, $default = null)
{
if (isset($this->meta[$key])) {
return $this->meta[$key];
} else {
return $default;
}
}
public function setMeta($key, $value)
{
$meta = $this->meta;
$meta[$key] = $value;
$this->meta = $meta;
}
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',
// ];
}

View File

@@ -25,6 +25,20 @@ class Helper
*/
const QUEUE_DEFAULT = 'default';
/**
* Stores list of global entities (for caching).
*/
public static $global_entities = [];
/**
* Files with such extensions are being renamed on upload.
*/
public static $restricted_extensions = [
'php.*',
'sh',
'pl',
];
/**
* Menu structure used to display active menu item.
* Array are mnemonic names, strings - route names.
@@ -38,7 +52,7 @@ class Helper
'conversations.view',
'conversations.create',
'conversations.draft',
'conversations.search',
//'conversations.search',
],
'manage' => [
'settings' => 'settings',
@@ -462,6 +476,16 @@ class Helper
}
}
public static function setGlobalEntity($name, $entity)
{
self::$global_entities[$name] = $entity;
}
public static function getGlobalEntity($name)
{
return self::$global_entities[$name] ?? null;
}
/**
* Remove from text all tags, double spaces, etc.
*/
@@ -469,7 +493,7 @@ class Helper
{
// Remove all kinds of spaces after tags.
// https://stackoverflow.com/questions/3230623/filter-all-types-of-whitespace-in-php
$text = preg_replace("/^(.*)>[\r\n]*\s+/mu", '$1>', $text);
$text = preg_replace("/^(.*)>[\r\n]*\s+/mu", '$1>', $text ?? '');
// Remove <script> and <style> blocks.
$text = preg_replace('#<script(.*?)>(.*?)</script>#is', '', $text);
@@ -552,7 +576,7 @@ class Helper
}
} else {
if ($current_route == $secondary_routes) {
return $primary_name == $menu_item_name;
return $primary_name == $menu_item_name || $menu_item_name == $secondary_routes;
}
}
}
@@ -582,8 +606,14 @@ class Helper
if (preg_match('/png/i', $mime_type)) {
$src = imagecreatefrompng($file);
$kek = imagecolorallocate($src, 255, 255, 255);
imagefill($src, 0, 0, $kek);
} elseif (preg_match('/gif/i', $mime_type)) {
$src = imagecreatefromgif($file);
$kek = imagecolorallocate($src, 255, 255, 255);
imagefill($src, 0, 0, $kek);
} elseif (preg_match('/bmp/i', $mime_type)) {
$src = imagecreatefrombmp($file);
} else {
@@ -609,10 +639,11 @@ class Helper
// Resize and crop
imagecopyresampled($thumb,
$src,
0 - ($new_width - $thumb_width) / 2, // Center the image horizontally
0 - ($new_height - $thumb_height) / 2, // Center the image vertically
ceil(0 - ($new_width - $thumb_width) / 2), // Center the image horizontally
ceil(0 - ($new_height - $thumb_height) / 2), // Center the image vertically
0, 0,
$new_width, $new_height,
ceil($new_width),
ceil($new_height),
$width, $height);
imagedestroy($src);
@@ -645,36 +676,39 @@ class Helper
* Create zip archive.
* Source example: public/files/*
* File name example: test.zip.
* storage_path without app/
*/
public static function createZipArchive($source, $file_name, $folder = '')
public static function createZipArchive($source, $file_name, $folder = '', $storage_file_path = '')
{
if (!$source || !$file_name) {
return false;
}
$files = glob($source);
//$dest_folder = storage_path().DIRECTORY_SEPARATOR.'app/zipper';
// if (!file_exists($dest_folder)) {
// $mkdir_result = File::makeDirectory($dest_folder, 0755);
// if (!$mkdir_result) {
// return false;
// }
// }
$storage_path = 'app/zipper'.DIRECTORY_SEPARATOR.$file_name;
$dest_path = storage_path().DIRECTORY_SEPARATOR.$storage_path;
if (!$storage_file_path) {
$storage_file_path = 'zipper'.DIRECTORY_SEPARATOR.$file_name;
} else {
// if (!self::getPrivateStorage()->exists($storage_path)) {
// self::getPrivateStorage()->makeDirectory($storage_path);
// }
}
$dest_path = storage_path().DIRECTORY_SEPARATOR.'app'.DIRECTORY_SEPARATOR.$storage_file_path;
// If file exists it has to be deleted, otherwise Zipper will add file to the existing archive
// By some reason \Storage not finding $storage_path file
// todo: use \Storage
if (\File::exists($dest_path)) {
\File::delete($dest_path);
if (self::getPrivateStorage()->exists($storage_file_path)) {
self::getPrivateStorage()->delete($storage_file_path);
}
\Chumper\Zipper\Facades\Zipper::make($dest_path)->folder($folder)->add($files)->close();
return $dest_path;
}
public static function getPrivateStorage()
{
return \Storage::disk('private');
}
public static function formatException($e)
{
return 'Error: '.$e->getMessage().'; File: '.$e->getFile().' ('.$e->getLine().')';
@@ -703,7 +737,7 @@ class Helper
public static function downloadRemoteFile($url, $destinationFilePath)
{
$client = new \GuzzleHttp\Client();
$client->request('GET', $url, [
'sink' => $destinationFilePath,
'connect_timeout' => 7,
@@ -802,6 +836,13 @@ class Helper
}
}
public static function setLocale($locale)
{
if (in_array($locale, config('app.locales'))) {
app()->setLocale($locale);
}
}
/**
* Get locale's data.
*
@@ -847,9 +888,13 @@ class Helper
$env_path = app()->environmentFilePath();
$contents = file_get_contents($env_path);
// Maybe validate value and add quotes.
// str_contains($key, '=')
// !preg_match('/^[a-zA-Z_]+$/', $key)
if (strstr($value, '"')) {
// Escape quotes.
$value = '"'.str_replace('"', '\"', $value).'"';
} elseif (!preg_match('/^[a-zA-Z0-9_]+$/', $value) && $value !== '') {
// Add quotes.
$value = '"'.$value.'"';
}
$old_value = '';
// Match the given key at the beginning of a line
@@ -921,6 +966,8 @@ class Helper
*/
public static function backgroundAction($action, $params, $delay = 0)
{
$delay = \Eventy::filter('backgound_action.dispatch_delay', $delay, $action, $params);
$job = \App\Jobs\TriggerAction::dispatch($action, $params);
if ($delay) {
$job->delay($delay);
@@ -933,12 +980,17 @@ class Helper
*
* @param [type] $text [description]
*/
public static function htmlToText($text)
public static function htmlToText($text, $embed_images = false, $options = ['width' => 0])
{
// Process blockquotes.
$text = str_ireplace('<blockquote>', '<div>', $text);
$text = str_ireplace('</blockquote>', '</div>', $text);
return (new \Html2Text\Html2Text($text))->getText();
if ($embed_images) {
// Replace embedded images with their urls.
$text = preg_replace( '/<img\b[^>]*src=\"([^>"]+)\"[^>]*>/i', "<div>$1</div>", $text);
}
return (new \Html2Text\Html2Text($text, $options))->getText();
}
/**
@@ -994,7 +1046,7 @@ class Helper
$subdirectory = $_SERVER['SCRIPT_NAME'];
} elseif (basename($_SERVER['PHP_SELF']) === $filename) {
$subdirectory = $_SERVER['PHP_SELF'];
} elseif (basename($_SERVER['ORIG_SCRIPT_NAME']) === $filename) {
} elseif (array_key_exists('ORIG_SCRIPT_NAME', $_SERVER) && basename($_SERVER['ORIG_SCRIPT_NAME']) === $filename) {
$subdirectory = $_SERVER['ORIG_SCRIPT_NAME']; // 1and1 shared hosting compatibility
} else {
// Backtrack up the script_filename to find the portion matching
@@ -1014,6 +1066,10 @@ class Helper
}
}
if ($subdirectory === null) {
$subdirectory = '';
}
$subdirectory = str_replace('public/index.php', '', $subdirectory);
$subdirectory = str_replace('index.php', '', $subdirectory);
@@ -1187,6 +1243,10 @@ class Helper
public static function purifyHtml($html)
{
if (!$html) {
return $html;
}
$html = \Purifier::clean($html);
// Remove all kinds of spaces after tags
@@ -1201,7 +1261,7 @@ class Helper
*/
public static function safePassword($password)
{
return str_repeat("*", mb_strlen($password));
return str_repeat("*", mb_strlen($password ?? ''));
}
/**
@@ -1264,10 +1324,10 @@ class Helper
*/
public static function getWorkerIdentifier()
{
return md5(config('app.key'));
return md5(config('app.key') ?? '');
}
public static function uploadFile($file, $allowed_exts = [], $allowed_mimes = [])
public static function uploadFile($file, $allowed_exts = [], $allowed_mimes = [], $sanitize_file_name = true)
{
$ext = strtolower($file->getClientOriginalExtension());
@@ -1283,11 +1343,15 @@ class Helper
throw new \Exception(__('Unsupported file type'), 1);
}
}
$name = \Str::random(25).'.'.$ext;
$file_name = \Str::random(25).'.'.$ext;
$file->storeAs('uploads', $name);
if ($sanitize_file_name) {
$file_name = \Helper::sanitizeUploadedFileName($file_name);
}
return self::uploadedFilePath($name);
$file->storeAs('uploads', $file_name);
return self::uploadedFilePath($file_name);
}
public static function uploadedFileRemove($name)
@@ -1333,4 +1397,225 @@ class Helper
\Session::flash('flashes_floating', $flashes);
}
public static function isMySql()
{
return \DB::connection()->getPDO()->getAttribute(\PDO::ATTR_DRIVER_NAME) == 'mysql';
}
public static function isPgSql()
{
return \DB::connection()->getPDO()->getAttribute(\PDO::ATTR_DRIVER_NAME) == 'pgsql';
}
public static function humanFileSize($size, $unit="")
{
if ((!$unit && $size >= 1<<30) || $unit == "GB") {
return number_format($size/(1<<30),2)."GB";
}
if ((!$unit && $size >= 1<<20) || $unit == "MB") {
return number_format($size/(1<<20),2)."MB";
}
//if ((!$unit && $size >= 1<<10) || $unit == "KB") {
return number_format($size/(1<<10),2)."KB";
// }
// return number_format($size)." bytes";
}
public static function isPrint()
{
return (bool)app('request')->input('print');
}
public static function isDev()
{
return config('app.env') != 'production';
}
public static function substrUnicode($str, $s, $l = null)
{
return join("", array_slice(preg_split("//u", $str, -1, PREG_SPLIT_NO_EMPTY), $s, $l));
}
/**
* Disable sql_require_primary_key option to avoid errors when migrating.
* Only for MySQL.
*/
public static function disableSqlRequirePrimaryKey()
{
if (!self::isMySql()) {
return;
}
try {
\DB::statement('SET SESSION sql_require_primary_key=0');
} catch (\Exception $e) {
// General error: 1193 Unknown system variable 'sql_require_primary_key'.
// Do nothing.
}
}
public static function downloadRemoteFileAsTmp($uri)
{
try {
$headers = get_headers($uri);
if (!preg_match("/200/", $headers[0])) {
return false;
}
$contents = file_get_contents($uri);
if (!$contents) {
return false;
}
$temp_file = tempnam(sys_get_temp_dir(), 'fs');
\File::put($temp_file, $contents);
return $temp_file;
} catch (\Exception $e) {
\Helper::logException($e, 'Error downloading a remote file ('.$uri.'): ');
return false;
}
}
public static function downloadRemoteFileAsTmpFile($uri)
{
$file_path = self::downloadRemoteFileAsTmp($uri);
if ($file_path) {
return new \Illuminate\Http\UploadedFile(
$file_path, basename($file_path),
null, null, true
);
} else {
return null;
}
}
public static function sanitizeUploadedFileName($file_name)
{
// Check extension.
$extension = pathinfo($file_name, PATHINFO_EXTENSION);
if (preg_match('/('.implode('|', self::$restricted_extensions).')/', strtolower($extension))) {
// Add underscore to the extension if file has restricted extension.
$file_name = $file_name.'_';
}
return $file_name;
}
public static function remoteFileName($file_url)
{
return preg_replace("/\?.*/", '', basename($file_url));
}
public static function binaryDataMimeType($data)
{
$finfo = new \finfo(FILEINFO_MIME_TYPE);
return $finfo->buffer($data);
}
/**
* https://php.watch/versions/8.0/deprecated-reflectionparameter-methods
*/
public static function getClass($param)
{
return $param->getType() && !$param->getType()->isBuiltin() ? new \ReflectionClass(method_exists($param->getType(), 'getName') ? $param->getType()->getName() : $param->getClass()->name) : null;
}
/**
* https://php.watch/versions/8.0/deprecated-reflectionparameter-methods
*/
public static function getClassName($param)
{
return $param->getType() && !$param->getType()->isBuiltin() ? method_exists($param->getType(), 'getName') ? $param->getType()->getName() : $param->getClass()->name : null;
}
public static function getWebCronHash()
{
return md5(config('app.key').'web_cron_hash');
}
public static function fixProtocol($url)
{
if (parse_url(config('app.url'), PHP_URL_SCHEME) == 'http' && parse_url($url, PHP_URL_SCHEME) != 'http') {
return str_replace('https://', 'http://', $url);
}
if (parse_url(config('app.url'), PHP_URL_SCHEME) == 'https' && parse_url($url, PHP_URL_SCHEME) != 'https') {
return str_replace('http://', 'https://', $url);
}
return $url;
}
/**
* Fix and parse date to Carbon.
*/
public static function parseDateToCarbon($date)
{
if (preg_match('/\+0580/', $date)) {
$date = str_replace('+0580', '+0530', $date);
}
$date = trim(rtrim($date));
$date = preg_replace('/[<>]/', '', $date);
$date = str_replace('_', ' ', $date);
try {
return Carbon::parse($date);
} catch (\Exception $e) {
switch (true) {
case preg_match('/([0-9]{1,2}\ [A-Z]{2,3}\ [0-9]{4}\ [0-9]{1,2}\:[0-9]{1,2}\:[0-9]{1,2}\ UT)+$/i', $date) > 0:
case preg_match('/([A-Z]{2,3}\,\ [0-9]{1,2}\ [A-Z]{2,3}\ [0-9]{4}\ [0-9]{1,2}\:[0-9]{1,2}\:[0-9]{1,2}\ UT)+$/i', $date) > 0:
$date .= 'C';
break;
case preg_match('/([A-Z]{2,3}[\,|\ \,]\ [0-9]{1,2}\ [A-Z]{2,3}\ [0-9]{4}\ [0-9]{1,2}\:[0-9]{1,2}\:[0-9]{1,2}.*)+$/i', $date) > 0:
case preg_match('/([A-Z]{2,3}\,\ [0-9]{1,2}\ [A-Z]{2,3}\ [0-9]{4}\ [0-9]{1,2}\:[0-9]{1,2}\:[0-9]{1,2}\ [\-|\+][0-9]{4}\ \(.*)\)+$/i', $date) > 0:
case preg_match('/([A-Z]{2,3}\, \ [0-9]{1,2}\ [A-Z]{2,3}\ [0-9]{4}\ [0-9]{1,2}\:[0-9]{1,2}\:[0-9]{1,2}\ [\-|\+][0-9]{4}\ \(.*)\)+$/i', $date) > 0:
case preg_match('/([0-9]{1,2}\ [A-Z]{2,3}\ [0-9]{2,4}\ [0-9]{2}\:[0-9]{2}\:[0-9]{2}\ [A-Z]{2}\ \-[0-9]{2}\:[0-9]{2}\ \([A-Z]{2,3}\ \-[0-9]{2}:[0-9]{2}\))+$/i', $date) > 0:
$array = explode('(', $date);
$array = array_reverse($array);
$date = trim(array_pop($array));
break;
}
try {
return Carbon::parse($date);
} catch (\Exception $_e) {
return Carbon::now();
}
}
return null;
}
public static function urlHome()
{
return \Config::get('app.url');
// $url = rtrim($url, "/");
// return $url.'/home';
}
/**
* Request::url() may return URL with incorrect protocol.
* Use \Request::getRequestUri() instead.
*/
/*public static function currentUrl()
{
$url = \Request::urlFull();
if (\Str::startsWith(config('app.url'), 'http://') && !\Str::startsWith($url, 'http://')) {
$url = str_replace('https://', 'http://', $url);
}
if (\Str::startsWith(config('app.url'), 'https://') && !\Str::startsWith($url, 'https://')) {
$url = str_replace('http://', 'https://', $url);
}
return $url;
}*/
public static function isLocaleRtl(): bool
{
return in_array(app()->getLocale(), config("app.locales_rtl"));
}
}

View File

@@ -45,6 +45,8 @@ class Mail
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 a new separator.
* Order is not important.
@@ -57,6 +59,7 @@ class Mail
// 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
@@ -65,10 +68,10 @@ class Mail
'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.
'<blockquote', // General sepator
'regex:/<blockquote((?!quote)[^>])*>/', // General sepator. Should skip Gmail's <blockquote class="gmail_quote">.
'<!-- originalMessage -->',
'‐‐‐‐‐‐‐ Original Message ‐‐‐‐‐‐‐',
//'---Original---', // QQ separator, wait for emails from QQ and check
'--------------- Original Message ---------------',
];
/**
@@ -80,13 +83,15 @@ class Mail
* Configure mail sending parameters.
*
* @param App\Mailbox $mailbox
* @param App\User $user_from
* @param App\Conversation $conversation
*/
public static function setMailDriver($mailbox = null, $user_from = null)
public static function setMailDriver($mailbox = null, $user_from = null, $conversation = null)
{
if ($mailbox) {
// Configure mail driver according to Mailbox settings
\Config::set('mail.driver', $mailbox->getMailDriverName());
\Config::set('mail.from', $mailbox->getMailFrom($user_from));
\Config::set('mail.from', $mailbox->getMailFrom($user_from, $conversation));
// SMTP
if ($mailbox->out_method == Mailbox::OUT_METHOD_SMTP) {
@@ -158,7 +163,7 @@ class Mail
\Config::set('mail.password', null);
} else {
\Config::set('mail.username', Option::get('mail_username'));
\Config::set('mail.password', Option::get('mail_password'));
\Config::set('mail.password', \Helper::decrypt(Option::get('mail_password')));
}
\Config::set('mail.encryption', Option::get('mail_encryption'));
}
@@ -188,6 +193,7 @@ class Mail
$vars['{%customer.fullName%}'] = $data['customer']->getFullName(true);
$vars['{%customer.firstName%}'] = $data['customer']->getFirstName(true);
$vars['{%customer.lastName%}'] = $data['customer']->last_name;
$vars['{%customer.company%}'] = $data['customer']->company;
}
if (!empty($data['user'])) {
$vars['{%user.fullName%}'] = $data['user']->getFullName();
@@ -199,9 +205,11 @@ class Mail
$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);
$vars[$i] = htmlspecialchars($var ?? '');
}
}
@@ -296,15 +304,7 @@ class Mail
*/
public static function fetchTest($mailbox)
{
$client = new 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(),
]);
$client = \MailHelper::getMailboxClient($mailbox);
// Connect to the Server
$client->connect();
@@ -318,7 +318,11 @@ class Mail
// Get unseen messages for a period
$messages = $folder->query()->unseen()->since(now()->subDays(1))->leaveUnread()->get();
$last_error = $client->getLastError();
$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
@@ -349,7 +353,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) {
@@ -443,7 +447,7 @@ class Mail
*/
public static function fetchMessageMarkerValue($body)
{
preg_match('/{#FS:([^#]+)#}/', $body, $matches);
preg_match('/{#FS:([^#]+)#}/', $body ?? '', $matches);
if (!empty($matches[1]) && base64_decode($matches[1])) {
// Return first found marker.
return base64_decode($matches[1]);
@@ -465,8 +469,10 @@ class Mail
'x-autoreply' => '',
'x-autorespond' => '',
'auto-submitted' => 'auto-replied',
'precedence' => ['auto_reply', 'bulk', 'junk'],
'x-precedence' => ['auto_reply', 'bulk', 'junk'],
];
$headers = explode("\n", $headers_str);
$headers = explode("\n", $headers_str ?? '');
foreach ($autoresponder_headers as $auto_header => $auto_header_value) {
foreach ($headers as $header) {
@@ -480,6 +486,12 @@ class Mail
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;
}
@@ -552,23 +564,76 @@ class Mail
*/
public static function getMailboxClient($mailbox)
{
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(),
]);
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)
public static function generateMessageId($email_address, $raw_body = '')
{
return 'fsdummy-'.str_random(16).'@'.preg_replace("/.*@/", '', $email_address);
$hash = str_random(16);
if ($raw_body) {
$hash = md5(strval($raw_body));
}
return 'fs-'.$hash.'@'.preg_replace("/.*@/", '', $email_address);
}
/**
@@ -586,10 +651,11 @@ class Mail
$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 = array_merge(['INBOX'], $mailbox->getInImapFolders());
$imap_folders = $mailbox->getInImapFolders();
foreach ($imap_folders as $folder_name) {
try {
@@ -603,7 +669,10 @@ class Mail
$messages = $query->get();
$last_error = $client->getLastError();
$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.
@@ -617,10 +686,137 @@ class Mail
}
} catch (\Exception $e) {
// Do nothing.
\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,16 +2,15 @@
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_LICENSES = 'check_licenses';
const ACTION_ACTIVATE_LICENSE = 'activate_license';
const ACTION_DEACTIVATE_LICENSE = 'deactivate_license';
const ACTION_GET_VERSION = 'get_version';
@@ -24,7 +23,30 @@ class WpApi
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) {
if (strstr($url, '?')) {
$url .= '&';
} else {
$url .= '?';
}
$url .= 'v='.config('app.version');
return $client->request('POST', $url, [
'connect_timeout' => 10,
'form_params' => $params,
]);
} else {
$params['v'] = config('app.version');
return $client->request('GET', $url, [
'connect_timeout' => 10,
'query' => $params,
]);
}
}
@@ -35,14 +57,13 @@ class WpApi
{
self::$lastError = null;
$options = ['connect_timeout' => 7]; // seconds
try {
$response = Zttp::withOptions($options)->$method(self::url($endpoint, $alternative_api), $params);
$response = self::httpRequest($method, self::url($endpoint, $alternative_api), $params);
} catch (\Exception $e) {
if (!$alternative_api) {
return self::request($method, $endpoint, $params, true);
}
\Helper::logException($e, 'WpApi');
self::$lastError = [
'code' => $e->getCode(),
'message' => $e->getMessage(),
@@ -52,8 +73,9 @@ class WpApi
}
// https://guzzle3.readthedocs.io/http-client/response.html
if ($response->status() < 500) {
$json = $response->json();
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
) {
@@ -92,6 +114,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.
*/

View File

@@ -181,7 +181,7 @@ class Module extends Model
{
$missing = [];
$list = explode(',', $required_extensions);
$list = explode(',', $required_extensions ?? '');
if (!is_array($list) || !count($list)) {
return [];
}
@@ -228,4 +228,32 @@ class Module extends Model
return $missing;
}
public static function formatName($name)
{
return preg_replace("/ Module($|.*\[.*\]$)/", '$1', $name);
}
public static function formatModuleData($module_data)
{
// Add (Third-Party).
if (\App\Module::isOfficial($module_data['authorUrl'])
&& $module_data['author'] != 'FreeScout'
&& mb_substr(trim($module_data['name']), -1) != ']'
) {
$module_data['name'] = $module_data['name'].' ['.__('Third-Party').']';
}
return $module_data;
}
public static function isThirdParty($module_data)
{
if (\App\Module::isOfficial($module_data['authorUrl'])
&& $module_data['author'] != 'FreeScout'
) {
return true;
} else {
return false;
}
}
}

View File

@@ -72,6 +72,9 @@ class BroadcastNotification extends Notification implements ShouldQueue
return $data;
}
// Try to convert to array.
$mediums = (array)$payload->mediums;
$thread = Thread::find($payload->thread_id);
if (empty($thread)) {
@@ -82,7 +85,7 @@ class BroadcastNotification extends Notification implements ShouldQueue
$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
@@ -114,7 +117,7 @@ class BroadcastNotification extends Notification implements ShouldQueue
}
// 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

@@ -86,18 +86,18 @@ class WebsiteNotification extends Notification implements ShouldQueue
}
// 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) {
@@ -129,14 +129,14 @@ class WebsiteNotification extends Notification implements ShouldQueue
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

@@ -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;
@@ -58,7 +62,11 @@ class ThreadObserver
// \Eventy::action('thread.created', $thread);
// }
\App\Events\RealtimeConvNewThread::dispatchSelf($thread);
\App\Events\RealtimeMailboxNewThread::dispatchSelf($conversation->mailbox_id);
// 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

@@ -32,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.
*

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

@@ -38,7 +38,6 @@ class MailboxPolicy
if ($user->isAdmin()) {
return true;
} else {
// Use cached users for Realtime events
if ($mailbox->users->contains($user)) {
return true;
} else {
@@ -68,6 +67,19 @@ class MailboxPolicy
}
}
/**
* 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.
*
@@ -78,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

@@ -20,9 +20,10 @@ class ThreadPolicy
*/
public function edit(User $user, Thread $thread)
{
if ($thread->created_by_user_id
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))
&& ($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 {

View File

@@ -35,7 +35,7 @@ class UserPolicy
*/
public function create(User $user)
{
if ($user->isAdmin()) {
if ($user->isAdmin() || $user->hasPermission(User::PERM_EDIT_USERS)) {
return true;
} else {
return false;
@@ -52,7 +52,11 @@ class UserPolicy
*/
public function update(User $user, User $model)
{
if ($user->isAdmin() || $user->id == $model->id) {
if ($user->isAdmin()
|| $user->id == $model->id
|| $user->hasPermission(User::PERM_EDIT_USERS)
|| $user->canManageMailbox($model->id)
) {
return true;
} else {
return false;
@@ -101,10 +105,17 @@ class UserPolicy
*/
public function viewMailboxMenu(User $user)
{
if ($user->isAdmin() || \Eventy::filter('user.can_view_mailbox_menu', false, $user)) {
return true;
} else {
return false;
}
return true;
// if ($user->isAdmin() || \Eventy::filter('user.can_view_mailbox_menu', false, $user)) {
// return true;
// // hasManageMailboxAccess creates an extra query on each page,
// // to avoid this we don't show Manage menu to users,
// // user can manage mailboxes from dashboard.
// } else if ($user->hasManageMailboxAccess()) {
// return true;
// } else {
// return false;
// }
}
}

View File

@@ -22,6 +22,7 @@ class AppServiceProvider extends ServiceProvider
\App\Mailbox::observe(\App\Observers\MailboxObserver::class);
// Eloquent events for this table are not called automatically, so need to be called manually.
//\App\MailboxUser::observe(\App\Observers\MailboxUserObserver::class);
\App\Email::observe(\App\Observers\EmailObserver::class);
\App\User::observe(\App\Observers\UserObserver::class);
\App\Conversation::observe(\App\Observers\ConversationObserver::class);
\App\Thread::observe(\App\Observers\ThreadObserver::class);
@@ -53,6 +54,10 @@ class AppServiceProvider extends ServiceProvider
// Process module registration error - disable module and show error to admin
\Eventy::addFilter('modules.register_error', function ($exception, $module) {
$msg = __('The :module_name module has been deactivated due to an error: :error_message', ['module_name' => $module->getName(), 'error_message' => $exception->getMessage()]);
\Log::error($msg);
// request() does is empty at this stage
if (!empty($_POST['action']) && $_POST['action'] == 'activate') {
@@ -60,7 +65,7 @@ class AppServiceProvider extends ServiceProvider
\App\Module::deactiveModule($module->getAlias());
\Session::flash('flashes_floating', [[
'text' => __('The :module_name module has been deactivated due to an error: :error_message', ['module_name' => $module->getName(), 'error_message' => $exception->getMessage()]),
'text' => $msg,
'type' => 'danger',
'role' => \App\User::ROLE_ADMIN,
]]);
@@ -73,7 +78,7 @@ class AppServiceProvider extends ServiceProvider
\App\Module::deactiveModule($module->getAlias());
\Session::flash('flashes_floating', [[
'text' => __('The :module_name module has been deactivated due to an error: :error_message', ['module_name' => $module->getName(), 'error_message' => $exception->getMessage()]),
'text' => $msg,
'type' => 'danger',
'role' => \App\User::ROLE_ADMIN,
]]);

View File

@@ -13,6 +13,10 @@ class EventServiceProvider extends ServiceProvider
* @var array
*/
protected $listen = [
'Illuminate\Mail\Events\MessageSending' => [
'App\Listeners\ProcessSwiftMessage',
],
'Illuminate\Auth\Events\Registered' => [
'App\Listeners\LogRegisteredUser',
],
@@ -64,6 +68,7 @@ class EventServiceProvider extends ServiceProvider
'App\Events\UserReplied' => [
'App\Listeners\SendReplyToCustomer',
'App\Listeners\SendNotificationToUsers',
'App\Listeners\RefreshConversations',
],
'App\Events\CustomerReplied' => [
@@ -73,6 +78,7 @@ class EventServiceProvider extends ServiceProvider
'App\Events\UserCreatedConversation' => [
'App\Listeners\SendReplyToCustomer',
'App\Listeners\SendNotificationToUsers',
'App\Listeners\RefreshConversations',
],
'App\Events\CustomerCreatedConversation' => [

View File

@@ -8,6 +8,7 @@
namespace App\Providers;
use App\Conversation;
use App\Notifications\BroadcastNotification;
use Carbon\Carbon;
use Illuminate\Broadcasting\BroadcastManager;
@@ -78,8 +79,8 @@ class PolycastServiceProvider extends ServiceProvider
$payload = $collection->map(function ($item, $key) use ($request) {
$created = \Carbon\Carbon::createFromFormat('Y-m-d H:i:s', $item->created_at);
$requested = \Carbon\Carbon::createFromFormat('Y-m-d H:i:s', $request->get('time'));
$item->channels = json_decode($item->channels);
$item->payload = json_decode($item->payload);
$item->channels = json_decode($item->channels, false);
$item->payload = json_decode($item->payload, false);
// Add extra data to the payload
// This works only if payload has medius and thread_id
$item->data = BroadcastNotification::fetchPayloadData($item->payload);
@@ -118,6 +119,13 @@ class PolycastServiceProvider extends ServiceProvider
$viewing_conversation_id = null;
if (!empty($request->data) && !empty($request->data['conversation_id'])) {
$viewing_conversation_id = $request->data['conversation_id'];
if (!empty($request->data['conversation_view_focus'])) {
$conversation = Conversation::find($request->data['conversation_id']);
if ($conversation) {
\Eventy::action('conversation.view.focus', $conversation);
}
}
}
if ($viewing_conversation_id) {

View File

@@ -35,7 +35,7 @@ class RouteServiceProvider extends ServiceProvider
*/
public function map()
{
$this->mapApiRoutes();
//$this->mapApiRoutes();
$this->mapWebRoutes();
@@ -71,11 +71,11 @@ class RouteServiceProvider extends ServiceProvider
*
* @return void
*/
protected function mapApiRoutes()
{
Route::prefix('api')
->middleware('api')
->namespace($this->namespace)
->group(base_path('routes/api.php'));
}
// protected function mapApiRoutes()
// {
// Route::prefix('api')
// ->middleware('api')
// ->namespace($this->namespace)
// ->group(base_path('routes/api.php'));
// }
}

View File

@@ -26,13 +26,13 @@ class Subscription extends Model
const EVENT_CONVERSATION_ASSIGNED_TO_ME = 2;
const EVENT_CONVERSATION_ASSIGNED = 6;
const EVENT_FOLLOWED_CONVERSATION_UPDATED = 13;
const EVENT_I_AM_MENTIONED = 14;
const EVENT_MY_TEAM_MENTIONED = 15;
//const EVENT_MY_TEAM_MENTIONED = 15;
// Notify me when a customer replies…
const EVENT_CUSTOMER_REPLIED_TO_UNASSIGNED = 4;
const EVENT_CUSTOMER_REPLIED_TO_MY = 3;
const EVENT_CUSTOMER_REPLIED_TO_ASSIGNED = 7;
// Notify me when another Help Scout user replies or adds a note…
// Notify me when another a user replies or adds a note…
const EVENT_USER_REPLIED_TO_UNASSIGNED = 8;
const EVENT_USER_REPLIED_TO_MY = 5;
const EVENT_USER_REPLIED_TO_ASSIGNED = 9;
@@ -41,6 +41,7 @@ class Subscription extends Model
const MEDIUM_EMAIL = 1; // This is also website notifications
const MEDIUM_BROWSER = 2; // Browser push notification
const MEDIUM_MOBILE = 3;
const MEDIUM_MENU = 10; // Notifications menu
public static $mediums = [
self::MEDIUM_EMAIL,
@@ -52,16 +53,14 @@ class Subscription extends Model
self::MEDIUM_EMAIL => [
self::EVENT_CONVERSATION_ASSIGNED_TO_ME,
self::EVENT_FOLLOWED_CONVERSATION_UPDATED,
self::EVENT_I_AM_MENTIONED,
self::EVENT_MY_TEAM_MENTIONED,
//self::EVENT_MY_TEAM_MENTIONED,
self::EVENT_CUSTOMER_REPLIED_TO_MY,
self::EVENT_USER_REPLIED_TO_MY,
],
self::MEDIUM_BROWSER => [
self::EVENT_CONVERSATION_ASSIGNED_TO_ME,
self::EVENT_FOLLOWED_CONVERSATION_UPDATED,
self::EVENT_I_AM_MENTIONED,
self::EVENT_MY_TEAM_MENTIONED,
//self::EVENT_MY_TEAM_MENTIONED,
self::EVENT_CUSTOMER_REPLIED_TO_MY,
self::EVENT_USER_REPLIED_TO_MY,
],
@@ -96,7 +95,12 @@ class Subscription extends Model
*/
public static function addDefaultSubscriptions($user_id)
{
self::saveFromArray(self::$default_subscriptions, $user_id);
self::saveFromArray(self::getDefaultSubscriptions(), $user_id);
}
public static function getDefaultSubscriptions()
{
return Option::get('subscription_defaults', self::$default_subscriptions);
}
/**
@@ -154,16 +158,16 @@ class Subscription extends Model
*/
public static function usersToNotify($event_type, $conversation, $threads, $mailbox_user_ids = null)
{
// We can not check imported here, as after conversation has been imported via API
// notifications has to be sent.
// if ($conversation->imported) {
// return true;
// }
$thread = $threads[0];
// Ignore imported threads.
if ($thread->imported) {
return true;
}
// Detect events
$events = [];
$thread = $threads[0];
$prev_thread = null;
if (!empty($threads[1])) {
$prev_thread = $threads[1];
@@ -199,8 +203,10 @@ class Subscription extends Model
$events[] = self::EVENT_USER_REPLIED_TO_UNASSIGNED;
}
break;
// todo: EVENT_I_AM_MENTIONED, EVENT_MY_TEAM_MENTIONED
}
$events = \Eventy::filter('subscription.events_by_type', $events, $event_type, $thread);
// Check if assigned user changed
$user_changed = false;
if ($event_type != self::EVENT_TYPE_ASSIGNED && $event_type != self::EVENT_TYPE_NEW) {
@@ -233,11 +239,14 @@ class Subscription extends Model
->whereIn('event', $events)
->get();
$subscriptions = \Eventy::filter('subscription.subscriptions', $subscriptions, $conversation, $events, $thread);
// Filter subscribers
$users_to_notify = [];
foreach ($subscriptions as $i => $subscription) {
// Actions on conversation where user is assignee
if (in_array($subscription->event, [self::EVENT_CONVERSATION_ASSIGNED_TO_ME, self::EVENT_CUSTOMER_REPLIED_TO_MY, self::EVENT_USER_REPLIED_TO_MY]) && $conversation->user_id != $subscription->user_id
if (in_array($subscription->event, [self::EVENT_CONVERSATION_ASSIGNED_TO_ME, self::EVENT_CUSTOMER_REPLIED_TO_MY, self::EVENT_USER_REPLIED_TO_MY])
&& ($conversation->user_id != $subscription->user_id && !\Eventy::filter('subscription.is_user_assignee', false, $subscription, $conversation))
) {
continue;
}
@@ -249,10 +258,32 @@ class Subscription extends Model
continue;
}
// Skip if user muted notifications for this mailbox
if ($subscription->user->isAdmin()) {
// Mute notifications for events not related directly to the user.
if (!in_array($subscription->event, [self::EVENT_CONVERSATION_ASSIGNED_TO_ME, self::EVENT_FOLLOWED_CONVERSATION_UPDATED, self::EVENT_CUSTOMER_REPLIED_TO_MY, self::EVENT_USER_REPLIED_TO_MY])
&& !\Eventy::filter('subscription.is_related_to_user', false, $subscription, $thread)
) {
$mailbox_settings = $conversation->mailbox->getUserSettings($subscription->user_id);
if (!empty($mailbox_settings->mute)) {
continue;
}
}
}
if (\Eventy::filter('subscription.filter_out', false, $subscription, $thread)) {
continue;
}
$users_to_notify[$subscription->medium][] = $subscription->user;
$users_to_notify[$subscription->medium] = array_unique($users_to_notify[$subscription->medium]);
}
// Add menu notifications, for example.
$users_to_notify = \Eventy::filter('subscription.users_to_notify', $users_to_notify, $event_type, $events, $thread);
return $users_to_notify;
}
@@ -295,7 +326,7 @@ class Subscription extends Model
$users_to_notify = self::usersToNotify($event['event_type'], $event['conversation'], $threads, $mailbox_user_ids);
if (!$users_to_notify) {
if (!$users_to_notify || !is_array($users_to_notify)) {
continue;
}
@@ -320,17 +351,20 @@ class Subscription extends Model
}
}
// - Notify by email
// - Real-time menu notification (uses same medium as for email)
// - Email notification (better to create them first)
if (!empty($notify[self::MEDIUM_EMAIL])) {
// Email notification (better to create them first)
foreach ($notify[self::MEDIUM_EMAIL] as $conversation_id => $notify_info) {
\App\Jobs\SendNotificationToUsers::dispatch($notify_info['users'], $notify_info['conversation'], $notify_info['threads'])
->delay($delay)
->onQueue('emails');
}
// - Menu notification (uses same medium as for email)
foreach ($notify[self::MEDIUM_EMAIL] as $notify_info) {
}
// - Menu notification (uses same medium as for email)
if (!empty($notify[self::MEDIUM_EMAIL]) || !empty($notify[self::MEDIUM_MENU])) {
$notify_menu = ($notify[self::MEDIUM_EMAIL] ?? []) + ($notify[self::MEDIUM_MENU] ?? []);
foreach ($notify_menu as $notify_info) {
$website_notification = new WebsiteNotification($notify_info['conversation'], self::chooseThread($notify_info['threads']));
$website_notification->delay($delay);
\Notification::send($notify_info['users'], $website_notification);

View File

@@ -2,6 +2,15 @@
namespace App;
use App\Events\ConversationStatusChanged;
use App\Events\ConversationUserChanged;
use App\Events\UserAddedNote;
use App\Events\CustomerCreatedConversation;
use App\Events\UserCreatedConversation;
use App\Events\UserCreatedConversationDraft;
use App\Events\UserCreatedThreadDraft;
use App\Events\UserReplied;
use App\Events\CustomerReplied;
use Illuminate\Database\Eloquent\Model;
class Thread extends Model
@@ -144,6 +153,12 @@ class Thread extends Model
self::SOURCE_TYPE_API => 'api',
];
// Metas.
const META_CONVERSATION_HISTORY = 'ch';
const META_PREV_CONVERSATION = 'pc';
const META_MERGED_WITH_CONV = 'mwc';
const META_MERGED_INTO_CONV = 'mic';
protected $dates = [
'opened_at',
'created_at',
@@ -278,9 +293,9 @@ class Thread extends Model
/**
* Convert body to plain text.
*/
public function getBodyAsText()
public function getBodyAsText($options = array())
{
return \Helper::htmlToText($this->body);
return \Helper::htmlToText($this->body, true, $options);
}
public function getBodyWithFormatedLinks(string $body = '') :string
@@ -529,7 +544,9 @@ class Thread extends Model
$person = '';
if ($this->type == self::TYPE_CUSTOMER) {
$person = $this->customer_cached->getFullName(true);
if ($this->customer_cached) {
$person = $this->customer_cached->getFullName(true);
}
} elseif ($this->state == self::STATE_DRAFT && !empty($this->edited_by_user_id)) {
// Draft
if (auth()->user() && $this->edited_by_user_id == auth()->user()->id) {
@@ -553,9 +570,10 @@ class Thread extends Model
/**
* Get action text.
* $by_user - user who performed the action.
* $person must be already escaped.
*/
public function getActionText($conversation_number = '', $escape = false, $strip_tags = false, $by_user = null, $person = '')
public function getActionText($conversation_number = '', $escape = false, $strip_tags = false, $by_user = null, $person = '', $viewed_by_user = null)
{
$did_this = '';
@@ -574,7 +592,7 @@ class Thread extends Model
$assignee = htmlspecialchars($assignee);
}
if ($conversation_number) {
$did_this = __(':person assigned :assignee convsersation #:conversation_number', ['assignee' => $assignee, 'conversation_number' => $conversation_number]);
$did_this = __(':person assigned :assignee conversation #:conversation_number', ['assignee' => $assignee, 'conversation_number' => $conversation_number]);
} else {
$did_this = __(":person assigned to :assignee", ['assignee' => $assignee]);
}
@@ -594,6 +612,21 @@ class Thread extends Model
$did_this = __(":person restored");
} elseif ($this->action_type == self::ACTION_TYPE_MOVED_FROM_MAILBOX) {
$did_this = __(":person moved conversation from another mailbox");
} elseif ($this->action_type == self::ACTION_TYPE_MERGED) {
if (!empty($this->getMeta(Thread::META_MERGED_WITH_CONV))) {
$did_this = __(":person merged with another conversation");
} else {
$merge_conversation = Conversation::find($this->getMeta(Thread::META_MERGED_INTO_CONV));
$merge_conversation_number = '';
if ($merge_conversation) {
$merge_conversation_number = $merge_conversation->number;
}
if ($merge_conversation) {
$did_this = __(":person merged into conversation #:conversation_number", ['conversation_number' => '<a href="'.$merge_conversation->url().'" class="link-black">'.$merge_conversation_number.'</a>']);
} else {
$did_this = __(":person merged into conversation #:conversation_number", ['conversation_number' => $merge_conversation_number]);
}
}
}
} elseif ($this->state == self::STATE_DRAFT) {
if (empty($this->edited_by_user_id)) {
@@ -613,7 +646,7 @@ class Thread extends Model
}
}
$did_this = \Eventy::filter('thread.action_text', $did_this, $this, $conversation_number, $escape);
$did_this = \Eventy::filter('thread.action_text', $did_this, $this, $conversation_number, $escape, $viewed_by_user);
if ($strip_tags) {
$did_this = strip_tags($did_this);
@@ -633,11 +666,11 @@ class Thread extends Model
/**
* Description of what happened.
*/
public function getActionDescription($conversation_number, $escape = true)
public function getActionDescription($conversation_number, $escape = true, $viewed_by_user = null)
{
// Person
$person = $this->getActionPerson($conversation_number);
$did_this = $this->getActionText($conversation_number);
$did_this = $this->getActionText($conversation_number, false, false, null, '', $viewed_by_user);
if ($escape) {
$person = htmlspecialchars($person);
@@ -859,6 +892,280 @@ class Thread extends Model
return $thread;
}
public static function createExtended($data = [], $conversation = null, $customer = null, $update_conv = true)
{
if (empty($data['type']) || empty($data['body'])) {
return false;
}
$is_customer = ($data['type'] == Thread::TYPE_CUSTOMER);
if (!$customer && !empty($data['customer_id'])) {
$customer = Customer::find($data['customer_id']);
}
if (!$customer) {
$customer = $conversation->customer;
}
// User which creatd the thread should be passed in created_by_user_id.
// user_id is check for backward compatibility.
$user_id = $data['created_by_user_id'] ?? $data['user_id'] ?? null;
// Check type.
if ($data['type'] == Thread::TYPE_CUSTOMER && empty($customer)) {
return false;
//return $this->getErrorResponse('`customer` parameter is required', 'customer');
}
if (($data['type'] == Thread::TYPE_MESSAGE || $data['type'] == Thread::TYPE_NOTE) && empty($user_id)) {
return false;
//return $this->getErrorResponse('`user` parameter is required', 'user');
}
// Create thread.
$now = date('Y-m-d H:i:s');
// New conversation.
$new = !$conversation->threads_count;
$thread = new Thread();
$thread->conversation_id = $conversation->id;
$thread->type = $data['type'];
if ($is_customer) {
$thread->source_via = Thread::PERSON_CUSTOMER;
$thread->created_by_customer_id = $customer->id;
} else {
$thread->source_via = Thread::PERSON_USER;
$thread->created_by_user_id = $user_id;
$thread->edited_by_user_id = null;
$thread->edited_at = null;
}
$thread->source_type = Thread::SOURCE_TYPE_API;
$thread->state = $data['state'] ?? Thread::STATE_PUBLISHED;
$thread->customer_id = $customer->id ?? $conversation->customer_id ?? null;
$thread->body = $data['body'];
if (!$is_customer) {
$thread->setTo([$customer->getMainEmail()]);
}
$cc = \MailHelper::sanitizeEmails($data['cc'] ?? []);
$thread->setCc($cc);
$bcc = \MailHelper::sanitizeEmails($data['bcc'] ?? []);
$thread->setBcc($bcc);
$thread->imported = (int)($data['imported'] ?? false);
if ($thread->imported && !empty($data['created_at'])) {
$thread->created_at = self::utcStringToServerDate($data['created_at']);
}
if ($new) {
$thread->first = true;
}
// Assignee.
if (empty($data['user_id'])) {
$thread->user_id = $conversation->user_id;
}
// Process attachments.
if (!empty($data['attachments'])) {
$has_attachments = false;
foreach ($data['attachments'] as $attachment) {
$content = null;
$uploaded_file = null;
if (is_object($attachment) && get_class($attachment) == 'Illuminate\Http\UploadedFile') {
$uploaded_file = $attachment;
$attachment = [];
$attachment['file_name'] = $uploaded_file->getClientOriginalName();
$attachment['mime_type'] = $uploaded_file->getMimeType();
} else {
if (empty($attachment['file_name'])
//|| empty($attachment['mime_type'])
|| (empty($attachment['data']) && empty($attachment['file_url']))
) {
continue;
}
if (!empty($attachment['data'])) {
// BASE64 string.
$content = base64_decode($attachment['data']);
if (!$content) {
continue;
}
if (empty($attachment['mime_type'])) {
$f = finfo_open();
$attachment['mime_type'] = finfo_buffer($f, $content, FILEINFO_MIME_TYPE);
}
} else {
// URL.
$uploaded_file = \Helper::downloadRemoteFileAsTmpFile($attachment['file_url']);
if (!$uploaded_file) {
continue;
}
if (empty($attachment['mime_type'])) {
$attachment['mime_type'] = $uploaded_file->getClientMimeType();
}
}
}
if (!$has_attachments) {
$thread->save();
}
$attachment = Attachment::create(
$attachment['file_name'],
$attachment['mime_type'],
null,
$content,
$uploaded_file = $uploaded_file,
$embedded = false,
$thread->id,
$user_id ?? null
);
if ($attachment) {
$has_attachments = true;
}
}
if ($has_attachments) {
$thread->has_attachments = true;
$conversation->has_attachments = true;
}
}
$thread->save();
if ($new) {
if ($is_customer) {
$conversation->source_via = Conversation::PERSON_CUSTOMER;
$conversation->created_by_customer_id = $customer->id;
} else {
$conversation->source_via = Conversation::PERSON_USER;
$conversation->created_by_user_id = $user_id;
}
}
$conversation->setCc($cc);
// BCC should keep BCC of the first email,
// so we change BCC only if it contains emails.
if ($bcc) {
$conversation->setBcc($bcc);
}
$update_folder = false;
if ($thread->isReply()) {
$conversation->last_reply_at = $now;
if ($is_customer) {
$conversation->last_reply_from = Conversation::PERSON_CUSTOMER;
// Set specific status
if (!empty($data['status'])) {
if ((int)$conversation->status != (int)$data['status']) {
$update_folder = true;
}
$conversation->status = $data['status'];
} else {
if ((int)$conversation->status != Conversation::STATUS_ACTIVE) {
$update_folder = true;
}
// Reply from customer makes conversation active
$conversation->status = Conversation::STATUS_ACTIVE;
}
} else {
$conversation->last_reply_from = Conversation::PERSON_USER;
$conversation->user_updated_at = $now;
if (!empty($data['status'])) {
if ((int)$conversation->status != (int)$data['status']) {
$update_folder = true;
}
$conversation->status = $data['status'];
} else {
if ((int)$conversation->status != Conversation::STATUS_PENDING) {
$update_folder = true;
}
// Reply from customer makes conversation active
$conversation->status = Conversation::STATUS_PENDING;
}
}
}
// Reply from customer to deleted conversation should undelete it.
if ($data['type'] == Thread::TYPE_CUSTOMER && $conversation->state == Conversation::STATE_DELETED) {
$conversation->state = Conversation::STATE_PUBLISHED;
$update_folder = true;
}
if ($update_conv) {
$conversation->customer_id = $customer->id;
if ($is_customer) {
$conversation->customer_email = $customer->getMainEmail();
}
if ($update_folder) {
$conversation->updateFolder();
}
}
// Update conversation here if needed.
if ($is_customer) {
if ($new) {
$conversation = \Eventy::filter('conversation.created_by_customer', $conversation, $thread, $customer);
} else {
$conversation = \Eventy::filter('conversation.customer_replied', $conversation, $thread, $customer);
}
}
// save() will check if something in the model has changed. If it hasn't it won't run a db query.
$conversation->save();
// Update folders counters
if (!$new) {
// Update folders counters
$conversation->mailbox->updateFoldersCounters();
}
// Events.
// Conversation customer changed
// Not used anywhere
// if ($prev_customer_id) {
// event(new ConversationCustomerChanged($conversation, $prev_customer_id, $prev_customer_email, null, $customer));
// }
if ($new) {
if ($is_customer) {
event(new CustomerCreatedConversation($conversation, $thread));
\Eventy::action('conversation.created_by_customer', $conversation, $thread, $customer);
} else {
// New conversation.
event(new UserCreatedConversation($conversation, $thread));
\Eventy::action('conversation.created_by_user_can_undo', $conversation, $thread);
// After Conversation::UNDO_TIMOUT period trigger final event.
\Helper::backgroundAction('conversation.created_by_user', [$conversation, $thread], now()->addSeconds(Conversation::UNDO_TIMOUT));
}
} elseif ($data['type'] == Thread::TYPE_NOTE) {
// Note.
event(new UserAddedNote($conversation, $thread));
\Eventy::action('conversation.note_added', $conversation, $thread);
} else {
// Reply.
if ($is_customer) {
event(new CustomerReplied($conversation, $thread));
\Eventy::action('conversation.customer_replied', $conversation, $thread, $customer);
} else {
event(new UserReplied($conversation, $thread));
\Eventy::action('conversation.user_replied_can_undo', $conversation, $thread);
// After Conversation::UNDO_TIMOUT period trigger final event.
\Helper::backgroundAction('conversation.user_replied', [$conversation, $thread], now()->addSeconds(Conversation::UNDO_TIMOUT));
}
}
return $thread;
}
/**
* Get full name of the user who edited thread.
*/
@@ -920,6 +1227,18 @@ class Thread extends Model
$this->setMetas($metas);
}
/**
* Unset thread meta value.
*/
public function unsetMeta($key)
{
$metas = $this->getMetas();
if (isset($metas[$key])) {
unset($metas[$key]);
$this->setMetas($metas);
}
}
/**
* Get full name of the user who forwarded conversation.
*/
@@ -1003,4 +1322,15 @@ class Thread extends Model
return $body;
}
public function getActionTypeName()
{
if (!$this->action_type) {
return '';
}
$action_types = \Eventy::filter('thread.action_types', self::$action_types);
return self::$action_types[$this->action_type] ?? '';
}
}

View File

@@ -6,6 +6,7 @@
namespace App;
use App\Email;
use App\Mail\PasswordChanged;
use App\Mail\UserInvite;
use App\Notifications\WebsiteNotification;
@@ -31,6 +32,8 @@ class User extends Authenticatable
const EMAIL_MAX_LENGTH = 100;
const EMAIL_DELETED_SUFFIX = '_deleted';
/**
* Roles.
*/
@@ -52,7 +55,7 @@ class User extends Authenticatable
* Statuses.
*/
const STATUS_ACTIVE = 1;
const STATUS_DISABLED = 2; // todo
const STATUS_DISABLED = 2;
const STATUS_DELETED = 3;
/**
@@ -72,15 +75,19 @@ class User extends Authenticatable
* Global user permissions.
*/
const PERM_DELETE_CONVERSATIONS = 1;
const PERM_EDIT_CONVERSATIONS = 2;
const PERM_EDIT_SAVED_REPLIES = 3;
const PERM_EDIT_TAGS = 4;
const PERM_EDIT_CONVERSATIONS = 2;
const PERM_EDIT_SAVED_REPLIES = 3;
const PERM_EDIT_TAGS = 4;
const PERM_EDIT_CUSTOM_FOLDERS = 5;
const PERM_EDIT_USERS = 10;
public static $user_permissions = [
self::PERM_DELETE_CONVERSATIONS,
self::PERM_EDIT_CONVERSATIONS,
self::PERM_EDIT_SAVED_REPLIES,
self::PERM_EDIT_TAGS,
self::PERM_EDIT_CUSTOM_FOLDERS,
self::PERM_EDIT_USERS,
];
const WEBSITE_NOTIFICATIONS_PAGE_SIZE = 25;
@@ -107,8 +114,12 @@ class User extends Authenticatable
*
* @var [type]
*/
protected $fillable = ['role', 'status', 'first_name', 'last_name', 'email', 'password', 'role', 'timezone', 'photo_url', 'type', 'emails', 'job_title', 'phone', 'time_format', 'enable_kb_shortcuts', 'locale'];
protected $fillable = ['role', 'status', 'first_name', 'last_name', 'email', 'password', 'timezone', 'photo_url', 'type', 'emails', 'job_title', 'phone', 'time_format', 'enable_kb_shortcuts', 'locale'];
protected $casts = [
'permissions' => 'array',
];
/**
* For array_unique function.
*
@@ -124,7 +135,7 @@ class User extends Authenticatable
*/
public function mailboxes()
{
return $this->belongsToMany('App\Mailbox')->as('settings')->withPivot('after_send');
return $this->belongsToMany('App\Mailbox');
}
/**
@@ -135,6 +146,15 @@ class User extends Authenticatable
return $this->mailboxes()->rememberForever();
}
public function mailboxesWithSettings()
{
return $this->belongsToMany('App\Mailbox')->as('settings')
->withPivot('after_send')
->withPivot('hide')
->withPivot('mute')
->withPivot('access');
}
/**
* Get conversations assigned to user.
*/
@@ -191,7 +211,7 @@ class User extends Authenticatable
*/
public function getFullName()
{
return $this->first_name.' '.$this->last_name;
return \Eventy::filter('user.full_name', $this->first_name.' '.$this->last_name, $this);
}
/**
@@ -211,19 +231,71 @@ class User extends Authenticatable
{
if ($this->isAdmin()) {
if ($cache) {
return Mailbox::rememberForever()->get();
$mailboxes = Mailbox::rememberForever()->get();
} else {
return Mailbox::all();
$mailboxes = Mailbox::all();
}
} else {
if ($cache) {
return $this->mailboxes_cached;
$mailboxes = $this->mailboxes_cached;
} else {
return $this->mailboxes;
$mailboxes = $this->mailboxes;
}
}
return $mailboxes->sortBy('name');
}
/**
* Get mailboxes to which user has access.
*/
public function mailboxesCanViewWithSettings($cache = false)
{
$user = $this;
if ($this->isAdmin()) {
$query = Mailbox::select(['mailboxes.*', 'mailbox_user.hide', 'mailbox_user.mute', 'mailbox_user.access'])
->leftJoin('mailbox_user', function ($join) use ($user) {
$join->on('mailbox_user.mailbox_id', '=', 'mailboxes.id');
$join->where('mailbox_user.user_id', $user->id);
});
} else {
$query = Mailbox::select(['mailboxes.*', 'mailbox_user.hide', 'mailbox_user.mute', 'mailbox_user.access'])
->join('mailbox_user', function ($join) use ($user) {
$join->on('mailbox_user.mailbox_id', '=', 'mailboxes.id');
$join->where('mailbox_user.user_id', $user->id);
});
}
if ($cache) {
return $query->rememberForever()->get();
} else {
return $query->get();
}
}
public function mailboxesSettings($cache = true)
{
$user = $this;
$query = MailboxUser::where('user_id', $user->id);
if ($cache) {
return $query->rememberForever()->get();
} else {
return $query->get();
}
}
public function mailboxSettings($mailbox_id)
{
$settings = $this->mailboxesSettings()->where('mailbox_id', $mailbox_id)->first();
if (!$settings) {
return Mailbox::getDummySettings();
}
return $settings;
}
/**
* Get IDs of mailboxes to which user has access.
*/
@@ -242,6 +314,53 @@ class User extends Authenticatable
return in_array($mailbox_id, $ids);
}
/**
* Check to see if the user can manage any mailboxes
*/
public function hasManageMailboxAccess() {
if ($this->isAdmin()) {
return true;
} else {
//$mailboxes = $this->mailboxesCanViewWithSettings(true);
$mailboxes = $this->mailboxesSettings();
foreach ($mailboxes as $mailbox) {
if (!empty($mailbox->access) && !empty(json_decode($mailbox->access))) {
return true;
}
};
}
}
/**
* Check to see if the user can manage a specific mailbox
*/
public function canManageMailbox($mailbox_id)
{
if ($this->isAdmin()) {
return true;
} else {
//$mailbox = $this->mailboxesCanViewWithSettings(true)->where('id', $mailbox_id)->first();
$mailbox = $this->mailboxesSettings()->where('mailbox_id', $mailbox_id)->first();
if ($mailbox && !empty(json_decode($mailbox->access ?? ''))) {
return true;
}
}
}
public function hasManageMailboxPermission($mailbox_id, $perm) {
if ($this->isAdmin()) {
return true;
} else {
//$mailbox = $this->mailboxesCanViewWithSettings(true)->where('id', $mailbox_id)->first();
$mailbox = $this->mailboxesSettings()->where('mailbox_id', $mailbox_id)->first();
if ($mailbox && !empty($mailbox->access) && in_array($perm, json_decode($mailbox->access))) {
return true;
}
}
}
/**
* Generate random password for the user.
*
@@ -305,11 +424,11 @@ class User extends Authenticatable
continue;
}
foreach (Folder::$personal_types as $type) {
$folder = new Folder();
$folder->mailbox_id = $mailbox_id;
$folder->user_id = $this->id;
$folder->type = $type;
$folder->save();
Folder::create([
'mailbox_id' => $mailbox_id,
'user_id' => $this->id,
'type' => $type,
]);
}
}
}
@@ -322,38 +441,53 @@ class User extends Authenticatable
*
* @return string
*/
public static function dateFormat($date, $format = 'M j, Y H:i', $user = null)
public static function dateFormat($date, $format = 'M j, Y H:i', $user = null, $modify_format = true, $use_user_timezone = true)
{
if (!$user) {
$user = auth()->user();
}
if (is_string($date)) {
// Convert string in to Carbon
$date = Carbon::parse($date);
try {
$date = Carbon::parse($date);
} catch (\Exception $e) {
$date = null;
}
}
if ($user) {
if ($user->time_format == self::TIME_FORMAT_12) {
$format = strtr($format, [
'H' => 'h',
'G' => 'g',
':i' => ':ia',
':ia:s' => ':i:sa',
]);
} else {
$format = strtr($format, [
'h' => 'H',
'g' => 'G',
':ia' => ':i',
':i:sa' => ':i:s',
]);
if (!$date) {
return '';
}
if (!$format) {
$format = 'M j, Y H:i';
}
if ($user && $user !== false) {
if ($modify_format) {
if ($user->time_format == self::TIME_FORMAT_12) {
$format = strtr($format, [
'H' => 'h',
'G' => 'g',
':i' => ':ia',
':ia:s' => ':i:sa',
]);
} else {
$format = strtr($format, [
'h' => 'H',
'g' => 'G',
':ia' => ':i',
':i:sa' => ':i:s',
]);
}
}
// todo: formatLocalized has to be used here and below,
// but it returns $format value instead of formatted date
return $date->setTimezone($user->timezone)->format($format);
} else {
return $date->format($format);
if ($use_user_timezone) {
$date->setTimezone($user->timezone);
}
}
return $date->format($format);
}
/**
@@ -390,7 +524,7 @@ class User extends Authenticatable
}
} else {
$diff_text = $date->diffForHumans();
$diff_text = preg_replace('/minutes?/', 'min', $diff_text);
$diff_text = preg_replace('/minute[sn]?/', 'min', $diff_text);
return $diff_text;
}
@@ -421,16 +555,18 @@ class User extends Authenticatable
public static function getUserPermissionName($user_permission)
{
$user_permission_names = [
self::PERM_DELETE_CONVERSATIONS => __('Users are allowed to delete notes/conversations'),
self::PERM_DELETE_CONVERSATIONS => __('Users are allowed to delete conversations'),
self::PERM_EDIT_CONVERSATIONS => __('Users are allowed to edit notes/replies'),
self::PERM_EDIT_SAVED_REPLIES => __('Users are allowed to edit/delete saved replies'),
self::PERM_EDIT_TAGS => __('Users are allowed to manage tags'),
self::PERM_EDIT_CUSTOM_FOLDERS => __('Users are allowed to manage custom folders'),
self::PERM_EDIT_USERS => __('Users are allowed to manage users'),
];
if (!empty($user_permission_names[$user_permission])) {
return $user_permission_names[$user_permission];
} else {
return \Event::fire('filter.user_permission_name', [$user_permission]);
return \Eventy::filter('user_permissions.name', '', $user_permission);
}
}
@@ -641,10 +777,17 @@ class User extends Authenticatable
/**
* Resize and save user photo.
*
* $uploaded_file can be \File or string.
*/
public function savePhoto($uploaded_file)
public function savePhoto($uploaded_file, $mime_type = '')
{
$resized_image = \App\Misc\Helper::resizeImage($uploaded_file->getRealPath(), $uploaded_file->getMimeType(), self::PHOTO_SIZE, self::PHOTO_SIZE);
$real_path = $uploaded_file;
if (!is_string($uploaded_file)) {
$real_path = $uploaded_file->getRealPath();
$mime_type = $uploaded_file->getMimeType();
}
$resized_image = \App\Misc\Helper::resizeImage($real_path, $mime_type, self::PHOTO_SIZE, self::PHOTO_SIZE);
if (!$resized_image) {
return false;
@@ -682,14 +825,44 @@ class User extends Authenticatable
$this->photo_url = '';
}
public function hasPermission($permission)
public function hasPermission($permission, $check_own_permissions = true)
{
$permissions = Option::get('user_permissions');
if (!empty($permissions) && is_array($permissions) && in_array($permission, $permissions)) {
return true;
} else {
return false;
$has_permission = false;
$global_permissions = self::getGlobalUserPermissions();
if (!empty($global_permissions) && is_array($global_permissions) && in_array($permission, $global_permissions)) {
$has_permission = true;
}
if ($check_own_permissions && !empty($this->permissions)) {
if (isset($this->permissions[$permission])) {
$has_permission = (bool)$this->permissions[$permission];
}
}
return $has_permission;
}
public static function getGlobalUserPermissions()
{
$permissions = [];
$permissions_json = config('app.user_permissions');
if ($permissions_json) {
$permissions_json = base64_decode($permissions_json);
try {
$permissions = json_decode($permissions_json, true);
} catch (\Exception $e) {
// Do nothing.
}
}
if (!is_array($permissions)) {
$permissions = [];
}
return $permissions;
}
/**
@@ -714,10 +887,7 @@ class User extends Authenticatable
return null;
}
$user->fill($data);
$user->password = \Hash::make($data['password']);
$user->email = Email::sanitizeEmail($data['email']);
$user->setData($data);
try {
$user->save();
@@ -728,6 +898,36 @@ class User extends Authenticatable
return $user;
}
/**
* Set fields.
*/
public function setData($data, $replace_data = true, $save = false)
{
if (isset($data['email'])) {
$data['email'] = Email::sanitizeEmail($data['email']);
}
if (isset($data['password'])) {
$data['password'] = \Hash::make($data['password']);
}
if ($replace_data) {
$this->fill($data);
} else {
// Update empty fields.
foreach ($data as $key => $value) {
if (in_array($key, $this->fillable) && empty($this->$key)) {
$this->$key = $value;
}
}
}
\Eventy::action('user.set_data', $this, $data, $replace_data);
if ($save) {
$this->save();
}
}
/**
* Check if current user's role is higher than passed.
*/
@@ -778,6 +978,16 @@ class User extends Authenticatable
return self::where('status', '!=', self::STATUS_DELETED);
}
public function isActive()
{
return $this->status == self::STATUS_ACTIVE;
}
public function isDisabled()
{
return $this->status == self::STATUS_DISABLED;
}
public function isDeleted()
{
return $this->status == self::STATUS_DELETED;
@@ -804,6 +1014,7 @@ class User extends Authenticatable
$join->on('mailbox_user.user_id', '=', 'users.id');
})
->whereIn('mailbox_user.mailbox_id', $mailbox_ids)
->groupBy('users.id')
->get();
}
@@ -848,4 +1059,45 @@ class User extends Authenticatable
return $users;
}
public static function getUserPermissionsList()
{
return \Eventy::filter('user_permissions.list', self::$user_permissions);
}
/**
* Check user main and alternate emails.
*/
public function hasEmail($email)
{
$email = Email::sanitizeEmail($email);
if ($this->email == $email) {
return true;
}
$alt_emails = explode(',', $this->emails ?? '');
foreach ($alt_emails as $alt_email) {
if (Email::sanitizeEmail($alt_email) == $email) {
return true;
}
}
return false;
}
/**
* Check if there is a mailbox with specified email.
*/
public static function mailboxEmailExists($email)
{
$email = Email::sanitizeEmail($email);
$mailbox = Mailbox::where('email', $email)->first();
if ($mailbox) {
return true;
} else {
return false;
}
}
}

19
artisan
View File

@@ -9,6 +9,25 @@ if (!version_compare(phpversion(), '7.0.0', '>=')) {
define('LARAVEL_START', microtime(true));
// PHP 8.1 fix.
if (! function_exists('e')) {
/**
* Escape HTML special characters in a string.
*
* @param \Illuminate\Contracts\Support\Htmlable|string $value
* @param bool $doubleEncode
* @return string
*/
function e($value, $doubleEncode = false)
{
if ($value instanceof \Illuminate\Contracts\Support\Htmlable) {
return $value->toHtml();
}
return htmlspecialchars($value ?? '', ENT_QUOTES, 'UTF-8', $doubleEncode);
}
}
/*
|--------------------------------------------------------------------------
| Register The Auto Loader

View File

@@ -10,14 +10,14 @@
}
],
"require": {
"php": ">=7.0.0",
"php": ">=7.1.0",
"symfony/debug": "v3.4.18",
"devfactory/minify": "1.0.7",
"fideloper/proxy": "3.3.4",
"laravel/framework": "v5.5.40",
"laravel/tinker": "v1.0.7",
"mews/purifier": "2.1.0",
"mews/purifier": "3.2.2",
"spatie/laravel-activitylog": "2.7.0",
"lord/laroute": "2.4.7",
"axn/laravel-laroute": "1.3.0",
@@ -29,22 +29,26 @@
"barryvdh/laravel-translation-manager": "v0.5.0",
"chumper/zipper": "v1.0.2",
"rachidlaasri/laravel-installer": "4.0.2",
"kitetail/zttp": "v0.4.0",
"rap2hpoutre/laravel-log-viewer": "v0.22.1",
"rap2hpoutre/laravel-log-viewer": "v2.0.0",
"codedge/laravel-selfupdater": "1.4.3",
"doctrine/dbal": "v2.5.13"
"doctrine/dbal": "2.12.1",
"egulias/email-validator": "2.1.10",
"symfony/console": "v3.4.47",
"javoscript/laravel-macroable-models": "1.0.4",
"ramsey/uuid": "3.9.6",
"symfony/polyfill-mbstring": "v1.24.0",
"webklex/php-imap": "dev-master#f565c3b2510238191981dec92840bfcf3cc8b7e8"
},
"require-dev": {
"barryvdh/laravel-debugbar": "v3.2.0",
"filp/whoops": "2.2.0",
"fzaninotto/faker": "v1.7.1",
"filp/whoops": "2.14.5",
"fzaninotto/faker": "v1.9.2",
"mockery/mockery": "1.1.0",
"phpunit/phpunit": "6.5.8",
"symfony/polyfill-ctype": "v1.10.*",
"vlucas/phpdotenv": "v2.5.1",
"symfony/css-selector": "v3.4.18",
"symfony/polyfill-mbstring": "v1.10.*",
"symfony/var-dumper": "v3.4.18",
"symfony/routing": "v3.4.18",
"symfony/process": "v3.4.18",
@@ -54,9 +58,7 @@
"psr/log": "1.0.*",
"symfony/http-kernel": "v3.4.18",
"symfony/finder": "v3.4.18",
"symfony/console": "v3.4.18",
"doctrine/lexer": "v1.0.1",
"egulias/email-validator": "2.1.6",
"swiftmailer/swiftmailer": "v6.1.*",
"symfony/translation": "v3.4.18",
"nesbot/carbon": "1.35.*",
@@ -64,14 +66,15 @@
"erusev/parsedown": "1.7.1",
"doctrine/inflector": "v1.2.*",
"guzzlehttp/psr7": "1.4.*",
"tedivm/jshrink": "v1.3.1",
"tedivm/jshrink": "1.4.0",
"nikic/php-parser": "v4.1.0",
"doctrine/annotations": "v1.4.*",
"doctrine/cache": "v1.6.*",
"doctrine/collections": "v1.4.*",
"doctrine/instantiator": "1.0.5",
"myclabs/deep-copy": "1.7.0",
"webmozart/assert": "1.3.0"
"webmozart/assert": "1.3.0",
"psr/container": "1.0.0"
},
"autoload": {
"classmap": [
@@ -79,32 +82,71 @@
"database/factories"
],
"psr-0": {
"Swift_": "overrides/Swiftmailer/lib/classes"
"Swift_": "overrides/swiftmailer/swiftmailer/lib/classes",
"HTMLPurifier_": "overrides/ezyang/htmlpurifier/library"
},
"fs-comment": "Above new lines are listed files containing custom updates. Below - only PHP compatibility updates.",
"psr-4": {
"App\\": "app/",
"Modules\\": "Modules/",
"Axn\\Laroute\\Routes\\": "overrides/Axn/Laroute/Routes/",
"Webklex\\IMAP\\": "overrides/Webklex/IMAP/",
"RachidLaasri\\LaravelInstaller\\Helpers\\": "overrides/RachidLaasri/LaravelInstaller/Helpers/",
"RachidLaasri\\LaravelInstaller\\Middleware\\": "overrides/RachidLaasri/LaravelInstaller/Middleware/",
"RachidLaasri\\LaravelInstaller\\Controllers\\": "overrides/RachidLaasri/LaravelInstaller/Controllers/",
"RachidLaasri\\LaravelInstaller\\Providers\\": "overrides/RachidLaasri/LaravelInstaller/Providers/",
"Nwidart\\Modules\\": "overrides/Nwidart/Modules/",
"Codedge\\Updater\\SourceRepositoryTypes\\": "overrides/Codedge/Updater/SourceRepositoryTypes/",
"Barryvdh\\TranslationManager\\": "overrides/Barryvdh/TranslationManager/",
"Illuminate\\Foundation\\": "overrides/Illuminate/Foundation/",
"Illuminate\\Routing\\": "overrides/Illuminate/Routing/",
"Illuminate\\Validation\\Concerns\\": "overrides/Illuminate/Validation/Concerns/",
"Illuminate\\Mail\\": "overrides/Illuminate/Mail/",
"Lord\\Laroute\\Routes\\": "overrides/Lord/Laroute/Routes/",
"TorMorten\\Eventy\\": "overrides/TorMorten/Eventy/",
"Symfony\\Component\\Debug\\": "overrides/Symfony/Component/Debug/"
"Axn\\Laroute\\Routes\\": "overrides/axn/laravel-laroute/src/Routes/",
"Webklex\\IMAP\\": "overrides/webklex/laravel-imap/src/IMAP/",
"RachidLaasri\\LaravelInstaller\\Helpers\\": "overrides/rachidlaasri/laravel-installer/src/Helpers/",
"RachidLaasri\\LaravelInstaller\\Middleware\\": "overrides/rachidlaasri/laravel-installer/src/Middleware/",
"RachidLaasri\\LaravelInstaller\\Controllers\\": "overrides/rachidlaasri/laravel-installer/src/Controllers/",
"RachidLaasri\\LaravelInstaller\\Providers\\": "overrides/rachidlaasri/laravel-installer/src/Providers/",
"Nwidart\\Modules\\": "overrides/nwidart/laravel-modules/src/",
"Codedge\\Updater\\SourceRepositoryTypes\\": "overrides/codedge/laravel-selfupdater/src/SourceRepositoryTypes/",
"Barryvdh\\TranslationManager\\": "overrides/barryvdh/laravel-translation-manager/src/",
"Illuminate\\Foundation\\": "overrides/laravel/framework/src/Illuminate/Foundation/",
"Illuminate\\Routing\\": "overrides/laravel/framework/src/Illuminate/Routing/",
"Illuminate\\Broadcasting\\Broadcasters\\": "overrides/laravel/framework/src/Illuminate/Broadcasting/Broadcasters/",
"Illuminate\\Validation\\Concerns\\": "overrides/laravel/framework/src/Illuminate/Validation/Concerns/",
"Illuminate\\Mail\\": "overrides/laravel/framework/src/Illuminate/Mail/",
"Illuminate\\Auth\\": "overrides/laravel/framework/src/Illuminate/Auth/",
"Illuminate\\Auth\\Middleware\\": "overrides/laravel/framework/src/Illuminate/Auth/Middleware/",
"Illuminate\\Container\\": "overrides/laravel/framework/src/Illuminate/Container/",
"Lord\\Laroute\\Routes\\": "overrides/lord/laroute/src/Routes/",
"TorMorten\\Eventy\\": "overrides/tormjens/eventy/src/",
"Symfony\\Component\\Debug\\": "overrides/symfony/debug/",
"Symfony\\Component\\HttpFoundation\\": "overrides/symfony/http-foundation/",
"Chumper\\Zipper\\Repositories\\": "overrides/chumper/zipper/src/Chumper/Zipper/Repositories/",
"Illuminate\\Support\\": "overrides/laravel/framework/src/Illuminate/Support/",
"Illuminate\\Http\\": "overrides/laravel/framework/src/Illuminate/Http/",
"Illuminate\\Database\\Eloquent\\": "overrides/laravel/framework/src/Illuminate/Database/Eloquent/",
"Illuminate\\Pagination\\": "overrides/laravel/framework/src/Illuminate/Pagination/",
"Illuminate\\Session\\": "overrides/laravel/framework/src/Illuminate/Session/",
"Carbon\\": "overrides/nesbot/carbon/src/Carbon/",
"Illuminate\\Cache\\": "overrides/laravel/framework/src/Illuminate/Cache/",
"Doctrine\\DBAL\\Driver\\": "overrides/doctrine/dbal/lib/Doctrine/DBAL/Driver/",
"Symfony\\Component\\Finder\\Iterator\\": "overrides/symfony/finder/Iterator/",
"Symfony\\Component\\Finder\\": "overrides/symfony/finder/",
"Symfony\\Component\\Console\\Helper\\": "overrides/symfony/console/Helper/",
"DebugBar\\": "overrides/maximebf/debugbar/src/DebugBar/",
"Illuminate\\Cache\\Console\\": "overrides/laravel/framework/src/Illuminate/Cache/Console/",
"Dotenv\\": "overrides/vlucas/phpdotenv/src/",
"Illuminate\\View\\": "overrides/laravel/framework/src/Illuminate/View/",
"Symfony\\Component\\Routing\\": "overrides/symfony/routing/",
"Symfony\\Component\\VarDumper\\Cloner\\": "overrides/symfony/var-dumper/Cloner/",
"Devfactory\\Minify\\Providers\\": "overrides/devfactory/minify/src/Providers/",
"Barryvdh\\Debugbar\\": "overrides/barryvdh/laravel-debugbar/src/",
"Barryvdh\\Debugbar\\DataFormatter\\": "overrides/barryvdh/laravel-debugbar/src/DataFormatter/",
"Symfony\\Component\\Process\\": "overrides/symfony/process/",
"Symfony\\Component\\HttpKernel\\Exception\\": "overrides/symfony/http-kernel/Exception/",
"Spatie\\String\\": "overrides/spatie/string/src/",
"GuzzleHttp\\": "overrides/guzzlehttp/guzzle/src/",
"GuzzleHttp\\Cookie\\": "overrides/guzzlehttp/guzzle/src/Cookie/",
"Ramsey\\Uuid\\": "overrides/ramsey/uuid/src/",
"Rap2hpoutre\\LaravelLogViewer\\": "overrides/rap2hpoutre/laravel-log-viewer/src/Rap2hpoutre/LaravelLogViewer/",
"Symfony\\Component\\Console\\Descriptor\\": "overrides/symfony/console/Descriptor",
"Symfony\\Component\\CssSelector\\XPath\\Extension\\": "overrides/symfony/css-selector/XPath/Extension/"
},
"exclude-from-classmap": [
"vendor/axn/laravel-laroute/src/Routes/Collection.php",
"vendor/webklex/laravel-imap/src/IMAP/Message.php",
"vendor/webklex/laravel-imap/src/IMAP/Client.php",
"vendor/rachidlaasri/laravel-installer/src/Helpers/RequirementsChecker.php",
"vendor/rachidlaasri/laravel-installer/src/Helpers/PermissionsChecker.php",
"vendor/rachidlaasri/laravel-installer/src/Helpers/EnvironmentManager.php",
"vendor/rachidlaasri/laravel-installer/src/Helpers/FinalInstallManager.php",
@@ -120,14 +162,87 @@
"vendor/barryvdh/laravel-translation-manager/src/Manager.php",
"vendor/barryvdh/laravel-translation-manager/src/Controller.php",
"vendor/laravel/framework/src/Illuminate/Foundation/ProviderRepository.php",
"vendor/laravel/framework/src/Illuminate/Foundation/PackageManifest.php",
"vendor/laravel/framework/src/Illuminate/Routing/UrlGenerator.php",
"vendor/laravel/framework/src/Illuminate/Routing/RouteSignatureParameters.php",
"vendor/laravel/framework/src/Illuminate/Routing/RouteDependencyResolverTrait.php",
"vendor/laravel/framework/src/Illuminate/Routing/Controller.php",
"vendor/laravel/framework/src/Illuminate/Validation/Concerns/ValidatesAttributes.php",
"vendor/laravel/framework/src/Illuminate/Mail/TransportManager.php",
"vendor/laravel/framework/src/Illuminate/Auth/SessionGuard.php",
"vendor/laravel/framework/src/Illuminate/Auth/Middleware/Authenticate.php",
"vendor/laravel/framework/src/Illuminate/Container/Container.php",
"vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php",
"vendor/laravel/framework/src/Illuminate/Broadcasting/Broadcasters/Broadcaster.php",
"vendor/rachidlaasri/laravel-installer/src/Providers/LaravelInstallerServiceProvider.php",
"vendor/lord/laroute/src/Routes/Collection.php",
"vendor/tormjens/eventy/src/Filter.php",
"vendor/tormjens/eventy/src/Action.php",
"vendor/symfony/debug/ExceptionHandler.php"
"vendor/symfony/debug/ExceptionHandler.php",
"vendor/symfony/http-foundation/ResponseHeaderBag.php",
"vendor/chumper/zipper/src/Chumper/Zipper/Repositories/ZipRepository.php",
"vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/URI/Host.php",
"vendor/laravel/framework/src/Illuminate/Support/Collection.php",
"vendor/laravel/framework/src/Illuminate/Http/Request.php",
"vendor/symfony/http-foundation/ParameterBag.php",
"vendor/symfony/http-foundation/HeaderBag.php",
"vendor/symfony/http-foundation/Response.php",
"vendor/symfony/http-foundation/AcceptHeader.php",
"vendor/symfony/http-foundation/FileBag.php",
"vendor/symfony/http-foundation/Request.php",
"vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php",
"vendor/laravel/framework/src/Illuminate/Pagination/AbstractPaginator.php",
"vendor/laravel/framework/src/Illuminate/Pagination/Paginator.php",
"vendor/laravel/framework/src/Illuminate/Pagination/LengthAwarePaginator.php",
"vendor/laravel/framework/src/Illuminate/Routing/RouteCollection.php",
"vendor/laravel/framework/src/Illuminate/Session/FileSessionHandler.php",
"vendor/nesbot/carbon/src/Carbon/Carbon.php",
"vendor/laravel/framework/src/Illuminate/Cache/Repository.php",
"vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOConnection.php",
"vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOQueryImplementation.php",
"vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOStatement.php",
"vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOStatementImplementations.php",
"vendor/laravel/framework/src/Illuminate/Support/Carbon.php",
"vendor/laravel/framework/src/Illuminate/Support/Str.php",
"vendor/laravel/framework/src/Illuminate/Support/ViewErrorBag.php",
"vendor/laravel/framework/src/Illuminate/Support/MessageBag.php",
"vendor/laravel/framework/src/Illuminate/Support/Optional.php",
"vendor/laravel/framework/src/Illuminate/Support/Fluent.php",
"vendor/laravel/framework/src/Illuminate/Database/Eloquent/Factory.php",
"vendor/symfony/finder/Finder.php",
"vendor/symfony/finder/Iterator/FilterIterator.php",
"vendor/symfony/finder/Iterator/FileTypeFilterIterator.php",
"vendor/symfony/finder/Iterator/RecursiveDirectoryIterator.php",
"vendor/symfony/finder/Iterator/ExcludeDirectoryFilterIterator.php",
"vendor/symfony/finder/Iterator/FilenameFilterIterator.php",
"vendor/symfony/finder/Iterator/PathFilterIterator.php",
"vendor/symfony/finder/Iterator/DepthRangeFilterIterator.php",
"vendor/symfony/finder/Iterator/DateRangeFilterIterator.php",
"vendor/symfony/console/Helper/HelperSet.php",
"vendor/maximebf/debugbar/src/DebugBar/DebugBar.php",
"vendor/laravel/framework/src/Illuminate/Cache/Console/ClearCommand.php",
"vendor/vlucas/phpdotenv/src/Loader.php",
"vendor/laravel/framework/src/Illuminate/View/View.php",
"vendor/symfony/routing/Route.php",
"vendor/symfony/routing/CompiledRoute.php",
"vendor/symfony/var-dumper/Cloner/Data.php",
"vendor/symfony/var-dumper/Cloner/Stub.php",
"vendor/devfactory/minify/src/Providers/BaseProvider.php",
"vendor/barryvdh/laravel-debugbar/src/DataFormatter/QueryFormatter.php",
"vendor/symfony/process/Process.php",
"vendor/symfony/http-kernel/Exception/HttpException.php",
"vendor/barryvdh/laravel-debugbar/src/JavascriptRenderer.php",
"vendor/spatie/string/src/Str.php",
"vendor/guzzlehttp/guzzle/src/Client.php",
"vendor/guzzlehttp/guzzle/src/Cookie/CookieJar.php",
"vendor/ramsey/uuid/src/Uuid.php",
"vendor/laravel/framework/src/Illuminate/Routing/Router.php",
"vendor/rap2hpoutre/laravel-log-viewer/src/Rap2hpoutre/LaravelLogViewer/LaravelLogViewer.php",
"vendor/symfony/console/Descriptor/TextDescriptor.php",
"vendor/symfony/console/Helper/Helper.php",
"vendor/symfony/finder/Iterator/SortableIterator.php",
"vendor/symfony/css-selector/XPath/Extension/NodeExtension.php"
]
},
"autoload-dev": {
@@ -149,6 +264,8 @@
"@php artisan key:generate"
],
"post-autoload-dump": [
"@php -r \"file_put_contents('vendor/composer/ClassLoader.php', preg_replace('/function includeFile.*/s', base64_decode('DQpmdW5jdGlvbiBpbmNsdWRlRmlsZSgkZmlsZSkNCnsNCiAgICB0cnkgew0KICAgICAgICBpbmNsdWRlICRmaWxlOw0KICAgIH0gY2F0Y2ggKFxFeGNlcHRpb24gJGUpIHsNCiAgICAgICAgJG1zZyA9ICRlLT5nZXRNZXNzYWdlKCk7DQogICAgICAgIGlmIChzdHJzdHIoJG1zZywgJ05vIHN1Y2ggZmlsZSBvciBkaXJlY3RvcnknKSkgew0KICAgICAgICAgICAgaWYgKHN0cnN0cigkbXNnLCAnL3ZlbmRvci9jb21wb3Nlci8uLi8uLi9vdmVycmlkZXMvJykpIHsNCiAgICAgICAgICAgICAgICAkbmV3X2ZpbGUgPSBzdHJfcmVwbGFjZSgnL3ZlbmRvci9jb21wb3Nlci8uLi8uLi9vdmVycmlkZXMvJywgJy92ZW5kb3IvY29tcG9zZXIvLi4vJywgJGZpbGUpOw0KICAgICAgICAgICAgfSBlbHNlIHsNCiAgICAgICAgICAgICAgICAkbmV3X2ZpbGUgPSBzdHJfcmVwbGFjZSgnL3ZlbmRvci9jb21wb3Nlci8uLi8nLCAnL3ZlbmRvci9jb21wb3Nlci8uLi8uLi9vdmVycmlkZXMvJywgJGZpbGUpOw0KICAgICAgICAgICAgfQ0KICAgICAgICAgICAgaW5jbHVkZSAkbmV3X2ZpbGU7DQogICAgICAgIH0gZWxzZSB7DQogICAgICAgICAgICB0aHJvdyAkZTsNCiAgICAgICAgfQ0KICAgIH0NCn0='), file_get_contents('vendor/composer/ClassLoader.php')));\"",
"@php -r \"!error_reporting(0) || array_map('unlink', json_decode(file_get_contents('composer.json'), true)['autoload']['exclude-from-classmap']);\"",
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
"@php artisan package:discover"
]

1777
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,11 @@
<?php
$key = env('APP_KEY', null);
$key_file = env("APP_KEY_FILE", null);
if (empty($key) && !empty($key_file)) {
$key = trim(file_get_contents($key_file));
}
return [
/*
@@ -12,7 +18,7 @@ return [
| or any other location as required by the application or its packages.
*/
'version' => '1.4.7',
'version' => '1.8.19',
/*
|--------------------------------------------------------------------------
@@ -92,7 +98,8 @@ return [
*/
'locale' => env('APP_LOCALE', 'en'),
'locales' => ['en', 'fr', 'de', 'it', 'pt-PT', 'pt-BR', 'ru', 'nl'],
'locales' => ['en', 'hr', 'cs', 'da', 'nl', 'fr', 'de', 'it', 'ja', 'ko', 'fa', 'pl', 'pt-PT', 'pt-BR', 'ru', 'es', 'sk', 'sv'],
'locales_rtl' => ['fa'],
'default_locale' => 'en',
/*
@@ -125,7 +132,7 @@ return [
|
*/
'key' => env('APP_KEY'),
'key' => $key,
'cipher' => 'AES-256-CBC',
@@ -193,7 +200,7 @@ return [
| PHP extensions required by the app
|-------------------------------------------------------------------------
*/
'required_extensions' => ['mysql / mysqli', 'mbstring', 'xml', 'imap', /*'mcrypt' mcrypt is deprecated*/ 'json', 'gd', 'fileinfo', 'openssl', 'zip', 'tokenizer'/*, 'dom', 'xmlwriter', 'libxml', 'phar'*/],
'required_extensions' => ['mysql / mysqli', 'mbstring', 'xml', 'imap', /*'mcrypt' mcrypt is deprecated*/ 'json', 'gd', 'fileinfo', 'openssl', 'zip', 'tokenizer', 'curl'/*, 'dom', 'xmlwriter', 'libxml', 'phar'*/],
/*
|--------------------------------------------------------------------------
@@ -247,6 +254,102 @@ return [
'open_tracking' => ['default' => true],
],
/*
|--------------------------------------------------------------------------
| php - attachments are downloaded via PHP.
|
| apache - attachments are downloaded via Apache's mod_xsendfile.
|
| nginx - attachments are downloaded via nginx's X-Accel-Redirect.
|-------------------------------------------------------------------------
*/
'download_attachments_via' => env('APP_DOWNLOAD_ATTACHMENTS_VIA', 'php'),
/*
|--------------------------------------------------------------------------
| File types which should be viewed in the browser instead of downloading.
| SVG images are not viewable to avid XSS.
|-------------------------------------------------------------------------
*/
'viewable_attachments' => env('APP_VIEWABLE_ATTACHMENTS', ['jpg', 'jpeg', 'jfif', 'pjpeg', 'pjp', 'apng', 'bmp', 'gif', 'ico', 'cur', 'png', 'tif', 'tiff', 'webp', 'pdf', 'txt', 'mp3', 'wav', 'ogg', 'wma']),
/*
|--------------------------------------------------------------------------
| Case insensitive regular expression, containing a list of
| mail server error responses, returned when a mail server can not deliver an email
| to one or more recipients. If FreeScout receives one of the listed
| error responses from the mail server, it does not try to resend the email
| to avoid sending multiple duplicate emails to other recipients.
|
| https://github.com/freescout-helpdesk/freescout/issues/870#issuecomment-786477909
|
|-------------------------------------------------------------------------
*/
'no_retry_mail_errors' => env('APP_NO_RETRY_MAIL_ERRORS', '(no valid recipients|does not comply with RFC)'),
/*
|--------------------------------------------------------------------------
| none - send to the customer only agent's reply in the email.
|
| last - send to the customer the last message in the email.
|
| full - send to the customer full conversation history in the email.
|
|-------------------------------------------------------------------------
*/
'email_conv_history' => env('APP_EMAIL_CONV_HISTORY', 'none'),
/*
|--------------------------------------------------------------------------
| none - send to the user only agent's reply in the email.
|
| last - send to the user the last message in the email.
|
| full - send to the user full conversation history in the email.
|
|-------------------------------------------------------------------------
*/
'email_user_history' => env('APP_EMAIL_USER_HISTORY', 'full'),
/*
|--------------------------------------------------------------------------
| JSON containing user permissions.
|
|-------------------------------------------------------------------------
*/
'user_permissions' => env('APP_USER_PERMISSIONS', ''),
/*
|--------------------------------------------------------------------------
| Dashboard path.
|
|-------------------------------------------------------------------------
*/
'dashboard_path' => env('APP_DASHBOARD_PATH', ''),
/*
|--------------------------------------------------------------------------
| Dashboard path.
|
|-------------------------------------------------------------------------
*/
'login_path' => env('APP_LOGIN_PATH', 'login'),
/*
|--------------------------------------------------------------------------
| Home page controller.
|
|-------------------------------------------------------------------------
*/
'home_controller' => env('APP_HOMEPAGE_CONTROLLER', 'SecureController@dashboard'),
/*
|--------------------------------------------------------------------------
| Disable update checker
|--------------------------------------------------------------------------
*/
'disable_updating' => env('APP_DISABLE_UPDATING', false),
/*
|--------------------------------------------------------------------------
| Autoloaded Service Providers

View File

@@ -41,10 +41,10 @@ return [
'provider' => 'users',
],
'api' => [
'driver' => 'token',
'provider' => 'users',
],
// 'api' => [
// 'driver' => 'token',
// 'provider' => 'users',
// ],
],
/*
@@ -96,6 +96,7 @@ return [
'provider' => 'users',
'table' => 'password_resets',
'expire' => 60,
'throttle' => 60,
],
],

View File

@@ -17,7 +17,8 @@ return [
|
*/
'core' => [
'minPhpVersion' => '7.0.0',
'minPhpVersion' => '7.1.0',
'maxPhpVersion' => '8.99.99',
],
'final' => [
'key' => false,
@@ -60,6 +61,7 @@ return [
'public/css/builds/' => '775',
'public/js/builds/' => '775',
'public/modules/' => '775',
'Modules/' => '775',
],
/*

View File

@@ -24,7 +24,7 @@ return [
'stubs' => [
'enabled' => true,
'path' => base_path().'/overrides/Nwidart/Modules/Commands/stubs',
'path' => base_path().'/overrides/nwidart/laravel-modules/src/Commands/stubs',
'files' => [
'start' => 'start.php',
'routes' => 'Http/routes.php',

View File

@@ -24,7 +24,7 @@ return [
'settings' => [
'default' => [
'HTML.Doctype' => 'HTML 4.01 Transitional',
'HTML.Allowed' => 'div,b,strong,i,em,u,a[href|title],ul,ol,li,p[style],br,span[style],img[width|height|alt|src],table,tr,td,th,thead,tfoot,tbody,blockquote',
'HTML.Allowed' => 'div,b,strong,i,em,u,a[href|title],ul,ol,li,p[style],br,span[style],img[width|height|alt|src],table,tr,td,th,thead,tfoot,tbody,blockquote,pre,h1,h2,h3,h4,h5,h6',
//'CSS.AllowedProperties' => 'font,font-size,font-weight,font-style,font-family,text-decoration,padding-left,color,background-color,text-align',
'CSS.AllowedProperties' => 'font-weight,font-style,text-decoration,color,background-color,text-align',
'AutoFormat.AutoParagraph' => true,

31
config/subscriptions.php Normal file
View File

@@ -0,0 +1,31 @@
<?php
return [
/*
|-------------------
| Default Notification Subscriptions
|----------------------
|
| This file is for configuring the default notification subscriptions for new users.
|
*/
'defaults' => [
\App\Subscription::MEDIUM_EMAIL => [
\App\Subscription::EVENT_CONVERSATION_ASSIGNED_TO_ME,
\App\Subscription::EVENT_FOLLOWED_CONVERSATION_UPDATED,
//\App\Subscription::EVENT_MY_TEAM_MENTIONED,
\App\Subscription::EVENT_CUSTOMER_REPLIED_TO_MY,
\App\Subscription::EVENT_USER_REPLIED_TO_MY,
],
\App\Subscription::MEDIUM_BROWSER => [
\App\Subscription::EVENT_CONVERSATION_ASSIGNED_TO_ME,
\App\Subscription::EVENT_FOLLOWED_CONVERSATION_UPDATED,
//\App\Subscription::EVENT_MY_TEAM_MENTIONED,
\App\Subscription::EVENT_CUSTOMER_REPLIED_TO_MY,
\App\Subscription::EVENT_USER_REPLIED_TO_MY,
],
],
];

View File

@@ -11,7 +11,7 @@ return [
|
*/
'route' => [
'prefix' => trim(parse_url(env('APP_URL', ''), PHP_URL_PATH), '/').'/translations',
'prefix' => trim(parse_url(env('APP_URL', ''), PHP_URL_PATH) ?: '', '/').'/translations',
'middleware' => [
'web',
'auth',

View File

@@ -8,6 +8,6 @@ $factory->define(App\Customer::class, function (Faker $faker) {
'first_name' => $faker->firstName,
'last_name' => $faker->lastName,
'job_title' => $faker->jobTitle,
'phones' => Customer::formatPhones([['value' => $faker->phoneNumber, 'type' => Customer::PHONE_TYPE_WORK]]),
'phones' => json_encode(Customer::formatPhones([['value' => $faker->phoneNumber, 'type' => Customer::PHONE_TYPE_WORK]])),
];
});

View File

@@ -13,6 +13,8 @@ class CreatePasswordResetsTable extends Migration
*/
public function up()
{
\Helper::disableSqlRequirePrimaryKey();
Schema::create('password_resets', function (Blueprint $table) {
$table->string('email', 191)->index();
$table->string('token', 255);

View File

@@ -39,7 +39,11 @@ class CreateCustomersTable extends Migration
// Indexes
// For ajax search
$table->index([DB::raw('first_name(80)'), DB::raw('last_name(80)')]);
if (DB::connection()->getPDO()->getAttribute(PDO::ATTR_DRIVER_NAME) == 'mysql') {
$table->index([DB::raw('first_name(80)'), DB::raw('last_name(80)')]);
} else {
$table->index(['first_name', 'last_name']);
}
});
}

View File

@@ -71,8 +71,13 @@ class CreateThreadsTable extends Migration
$table->timestamp('opened_at')->nullable();
$table->timestamps();
// https://github.com/laravel/framework/issues/9293#issuecomment-373229281
$table->unique([DB::raw('message_id(191)')], 'threads_message_id_index');
if (DB::connection()->getPDO()->getAttribute(PDO::ATTR_DRIVER_NAME) == 'mysql') {
// https://github.com/laravel/framework/issues/9293#issuecomment-373229281
$table->unique([DB::raw('message_id(191)')], 'threads_message_id_index');
} else {
$table->unique('message_id', 'threads_message_id_index');
}
// On conversation page
$table->index(['conversation_id', 'type', 'from', 'customer_id']);
$table->index(['conversation_id', 'created_at']);

View File

@@ -31,8 +31,13 @@ class CreateSendLogsTable extends Migration
$table->timestamps();
// Indexes
// https://github.com/laravel/framework/issues/9293#issuecomment-373229281
$table->index([DB::raw('message_id(191)')], 'send_logs_message_id_index');
if (DB::connection()->getPDO()->getAttribute(PDO::ATTR_DRIVER_NAME) == 'mysql') {
// https://github.com/laravel/framework/issues/9293#issuecomment-373229281
$table->index([DB::raw('message_id(191)')], 'send_logs_message_id_index');
} else {
$table->index(['message_id'], 'send_logs_message_id_index');
}
// Used when sending auto reply
$table->index(['customer_id', 'mail_type', 'created_at']);
});

View File

@@ -13,6 +13,8 @@ class CreateNotificationsTable extends Migration
*/
public function up()
{
\Helper::disableSqlRequirePrimaryKey();
Schema::create('notifications', function (Blueprint $table) {
$table->uuid('id')->primary();
$table->string('type');

View File

@@ -14,7 +14,6 @@ class AddInImapFoldersColumnToMailboxesTable extends Migration
public function up()
{
Schema::table('mailboxes', function (Blueprint $table) {
// Additional IMAP folders, INBOX folder is fetched by default.
$table->text('in_imap_folders')->nullable();
});
}

View File

@@ -0,0 +1,33 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddHideColumnToMailboxUserTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('mailbox_user', function (Blueprint $table) {
// Hide admin from Assign list.
$table->boolean('hide')->default(false);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('mailbox_user', function (Blueprint $table) {
$table->dropColumn('hide');
});
}
}

View File

@@ -0,0 +1,33 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddMuteColumnToMailboxUserTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('mailbox_user', function (Blueprint $table) {
// Mute notifications for events not directly related to the user
$table->boolean('mute')->default(false);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('mailbox_user', function (Blueprint $table) {
$table->dropColumn('mute');
});
}
}

View File

@@ -0,0 +1,45 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddPublicColumnToAttachmentsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('attachments', function (Blueprint $table) {
$table->boolean('public')->default(false);
});
DB::table('attachments')->update(['public' => true]);
$old_path = storage_path('app'.DIRECTORY_SEPARATOR.'public'.DIRECTORY_SEPARATOR.'attachment');
$new_path = storage_path('app'.DIRECTORY_SEPARATOR.'attachment');
// Move attachments.
try {
if (File::exists($old_path) && File::isDirectory($old_path) && !File::exists($new_path)) {
File::move($old_path, $new_path);
}
} catch (\Exception $e) {
\Log::error('Migration Error: '.$e->getMessage());
}
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('attachments', function (Blueprint $table) {
$table->dropColumn('public');
});
}
}

View File

@@ -0,0 +1,37 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class UpdateInImapFoldersInMailboxesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
// Add INBOX to the in_imap_folders.
$mailboxes = App\Mailbox::select(['id', 'in_imap_folders'])->get();
foreach ($mailboxes as $mailbox) {
$in_imap_folders = $mailbox->getInImapFolders();
if (count($in_imap_folders) && !in_array('INBOX', $in_imap_folders)) {
array_unshift($in_imap_folders, 'INBOX');
$mailbox->setInImapFolders($in_imap_folders);
$mailbox->save();
}
}
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
}
}

View File

@@ -0,0 +1,32 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddImapSentFolderColumnToMailboxesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('mailboxes', function (Blueprint $table) {
$table->string('imap_sent_folder', 25)->nullable();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('mailboxes', function (Blueprint $table) {
$table->dropColumn('imap_sent_folder');
});
}
}

View File

@@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class DropSlugColumnInMailboxesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
// slug field is not used.
Schema::table('mailboxes', function (Blueprint $table) {
// Index is dropped automatically.
$table->dropColumn('slug');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('mailboxes', function (Blueprint $table) {
$table->string('slug', 16)->unique()->nullable();
});
}
}

View File

@@ -0,0 +1,32 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddEmailHistoryColumnToConversationsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('conversations', function (Blueprint $table) {
$table->unsignedTinyInteger('email_history')->default(0);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('conversations', function (Blueprint $table) {
$table->dropColumn('email_history');
});
}
}

View File

@@ -0,0 +1,32 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddAccessColumnToMailboxUserTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('mailbox_user', function (Blueprint $table) {
$table->text('access')->nullable();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('mailbox_user', function (Blueprint $table) {
$table->dropColumn('access');
});
}
}

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