From 2dbbaf77397cf944039b7e6e72910d1959ae0225 Mon Sep 17 00:00:00 2001 From: Aleksander Machniak Date: Sun, 6 Nov 2016 09:39:16 +0100 Subject: [PATCH] Warn about too many disclosed recipients in composed email [max_disclosed_recipients] (#5132) Allow to omit a subject when sending an email (#5068) --- CHANGELOG | 2 + config/defaults.inc.php | 8 +- program/js/app.js | 102 +++++++++++++++++------- program/js/common.js | 19 +++-- program/localization/en_US/labels.inc | 1 + program/localization/en_US/messages.inc | 3 + program/steps/mail/compose.inc | 4 +- program/steps/mail/sendmail.inc | 11 ++- 8 files changed, 108 insertions(+), 42 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 5c92ba85f..1afbe2b29 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,8 @@ CHANGELOG Roundcube Webmail =========================== +- Allow to omit a subject when sending an email (#5068) +- Warn about too many disclosed recipients in composed email [max_disclosed_recipients] (#5132) - identity_select: Support Received header (#5085) - Plugin API: Added get_compose_responses hook (#5457) - Display error when trying to upload more files than specified in max_file_uploads (#5483) diff --git a/config/defaults.inc.php b/config/defaults.inc.php index 74cd611e7..4207f0b28 100644 --- a/config/defaults.inc.php +++ b/config/defaults.inc.php @@ -521,9 +521,15 @@ $config['sendmail_delay'] = 0; // Size in bytes (possible unit suffix: K, M, G) $config['max_message_size'] = '100M'; -// Maximum number of recipients per message. Default: 0 (no limit) +// Maximum number of recipients per message (including To, Cc, Bcc). +// Default: 0 (no limit) $config['max_recipients'] = 0; +// Maximum number of recipients per message exluding Bcc header. +// This is a soft limit, which means we only display a warning to the user. +// Default: 5 +$config['max_disclosed_recipients'] = 5; + // Maximum allowed number of members of an address group. Default: 0 (no limit) // If 'max_recipients' is set this value should be less or equal $config['max_group_members'] = 0; diff --git a/program/js/app.js b/program/js/app.js index 63c6894a5..24ae6410a 100644 --- a/program/js/app.js +++ b/program/js/app.js @@ -4345,14 +4345,23 @@ function rcube_webmail() }; // checks the input fields before sending a message - this.check_compose_input = function(cmd) + this.check_compose_input = function(cmd, skip_recipients_checks) { // check input fields - var input_to = $("[name='_to']"), + var key, recipients, dialog, + limit = this.env.max_disclosed_recipients, + input_to = $("[name='_to']"), input_cc = $("[name='_cc']"), input_bcc = $("[name='_bcc']"), input_from = $("[name='_from']"), - input_subject = $("[name='_subject']"); + input_subject = $("[name='_subject']"), + get_recipients = function(fields) { + fields = $.map(fields, function(v) { + v = $.trim(v.val()); + return v.length ? v : null; + }); + return fields.join(',').replace(/^[\s,;]+/, '').replace(/[\s,;]+$/, ''); + }; // check sender (if have no identities) if (input_from.prop('type') == 'text' && !rcube_check_email(input_from.val(), true)) { @@ -4362,53 +4371,92 @@ function rcube_webmail() } // check for empty recipient - var recipients = input_to.val() ? input_to.val() : (input_cc.val() ? input_cc.val() : input_bcc.val()); - if (!rcube_check_email(recipients.replace(/^\s+/, '').replace(/[\s,;]+$/, ''), true)) { + recipients = get_recipients([input_to, input_cc, input_bcc]); + if (!skip_recipients_checks && !rcube_check_email(recipients, true)) { alert(this.get_label('norecipientwarning')); input_to.focus(); return false; } // check if all files has been uploaded - for (var key in this.env.attachments) { + for (key in this.env.attachments) { if (typeof this.env.attachments[key] === 'object' && !this.env.attachments[key].complete) { alert(this.get_label('notuploadedwarning')); return false; } } + // check disclosed recipients limit + if (limit && !skip_recipients_checks && !this.env.disclosed_recipients_warned + && rcube_check_email(recipients = get_recipients([input_to, input_cc]), true, true) > limit + ) { + var save_func = function(move_to_bcc) { + if (move_to_bcc) { + var bcc = input_bcc.val(); + input_bcc.val((bcc ? (bcc + ', ') : '') + recipients).change(); + input_to.val('').change(); + input_cc.val('').change(); + } + + dialog.dialog('close'); + ref.check_compose_input(cmd, true); + }; + + dialog = this.show_popup_dialog( + this.get_label('disclosedrecipwarning'), + this.get_label('disclosedreciptitle'), + [{ + text: this.get_label('sendmessage'), + click: function() { save_func(false); }, + 'class': 'mainaction' + }, { + text: this.get_label('bccinstead'), + click: function() { save_func(true); } + }, { + text: this.get_label('cancel'), + click: function() { dialog.dialog('close'); } + }], + {dialogClass: 'warning'} + ); + + this.env.disclosed_recipients_warned = true; + return false; + } + // display localized warning for missing subject - if (input_subject.val() == '') { - var buttons = {}, - myprompt = $('
').html('
' + this.get_label('nosubjectwarning') + '
') - .appendTo(document.body), - prompt_value = $('').attr({type: 'text', size: 30}).val(this.get_label('nosubject')) - .appendTo(myprompt), + if (!this.env.nosubject_warned && input_subject.val() == '') { + var prompt_value = $('').attr({type: 'text', size: 40}), + myprompt = $('
') + .append($('
').text(this.get_label('nosubjectwarning'))) + .append(prompt_value), save_func = function() { input_subject.val(prompt_value.val()); - myprompt.dialog('close'); + dialog.dialog('close'); ref.command(cmd, { nocheck:true }); // repeat command which triggered this }; - buttons[this.get_label('sendmessage')] = function() { - save_func($(this)); - }; - buttons[this.get_label('cancel')] = function() { - input_subject.focus(); - $(this).dialog('close'); - }; - - myprompt.dialog({ - modal: true, - resizable: false, - buttons: buttons, - close: function(event, ui) { $(this).remove(); } - }); + dialog = this.show_popup_dialog( + myprompt, + this.get_label('nosubjecttitle'), + [{ + text: this.get_label('sendmessage'), + click: function() { save_func(); }, + 'class': 'mainaction' + }, { + text: this.get_label('cancel'), + click: function() { + input_subject.focus(); + dialog.dialog('close'); + } + }], + {dialogClass: 'warning'} + ); prompt_value.select().keydown(function(e) { if (e.which == 13) save_func(); }); + this.env.nosubject_warned = true; return false; } diff --git a/program/js/common.js b/program/js/common.js index e021820aa..2abc594f9 100644 --- a/program/js/common.js +++ b/program/js/common.js @@ -407,10 +407,14 @@ triggerEvent: function(evt, e) // check if input is a valid email address // By Cal Henderson // http://code.iamcal.com/php/rfc822/ -function rcube_check_email(input, inline) +function rcube_check_email(input, inline, count) { - if (input && window.RegExp) { - var qtext = '[^\\x0d\\x22\\x5c\\x80-\\xff]', + if (!input) + return count ? 0 : false; + + if (count) inline = true; + + var qtext = '[^\\x0d\\x22\\x5c\\x80-\\xff]', dtext = '[^\\x0d\\x5b-\\x5d\\x80-\\xff]', atom = '[^\\x00-\\x20\\x22\\x28\\x29\\x2c\\x2e\\x3a-\\x3c\\x3e\\x40\\x5b-\\x5d\\x7f-\\xff]+', quoted_pair = '\\x5c[\\x00-\\x7f]', @@ -443,12 +447,13 @@ function rcube_check_email(input, inline) delim = '[,;\\s\\n]', local_part = word+'(\\x2e'+word+')*', addr_spec = '(('+local_part+'\\x40'+domain+')|('+icann_addr+'))', - reg1 = inline ? new RegExp('(^|<|'+delim+')'+addr_spec+'($|>|'+delim+')', 'i') : new RegExp('^'+addr_spec+'$', 'i'); + rx_flag = count ? 'ig' : 'i', + rx = inline ? new RegExp('(^|<|'+delim+')'+addr_spec+'($|>|'+delim+')', rx_flag) : new RegExp('^'+addr_spec+'$', 'i'); - return reg1.test(input) ? true : false; - } + if (count) + return input.match(rx).length; - return false; + return rx.test(input); }; // recursively copy an object diff --git a/program/localization/en_US/labels.inc b/program/localization/en_US/labels.inc index e1d6ca7b7..84a268894 100644 --- a/program/localization/en_US/labels.inc +++ b/program/localization/en_US/labels.inc @@ -266,6 +266,7 @@ $labels['keyid'] = 'Key ID'; $labels['keylength'] = 'Bits'; $labels['keyexpired'] = 'Expired'; $labels['keyrevoked'] = 'Revoked'; +$labels['bccinstead'] = 'Use Bcc'; $labels['editidents'] = 'Edit identities'; $labels['spellcheck'] = 'Spell'; diff --git a/program/localization/en_US/messages.inc b/program/localization/en_US/messages.inc index 945bb22c2..916ae23dc 100644 --- a/program/localization/en_US/messages.inc +++ b/program/localization/en_US/messages.inc @@ -94,7 +94,10 @@ $messages['nonamewarning'] = 'Please enter a name.'; $messages['nopagesizewarning'] = 'Please enter a page size.'; $messages['nosenderwarning'] = 'Please enter sender email address.'; $messages['norecipientwarning'] = 'Please enter at least one recipient.'; +$messages['disclosedrecipwarning'] = 'All recipients will see each others e-mail addresses. To prevent this and protect their privacy you can use the Bcc field.'; +$messages['disclosedreciptitle'] = 'Too many public recipients'; $messages['nosubjectwarning'] = 'The "Subject" field is empty. Would you like to enter one now?'; +$messages['nosubjecttitle'] = 'No subject'; $messages['nobodywarning'] = 'Send this message without text?'; $messages['notsentwarning'] = 'Message has not been sent. Do you want to discard your message?'; $messages['restoresavedcomposedata'] = 'A previously composed but unsent message was found.\n\nSubject: $subject\nSaved: $date\n\nDo you want to restore this message?'; diff --git a/program/steps/mail/compose.inc b/program/steps/mail/compose.inc index 5d882b474..18411a3e2 100644 --- a/program/steps/mail/compose.inc +++ b/program/steps/mail/compose.inc @@ -88,7 +88,8 @@ $OUTPUT->add_label('nosubject', 'nosenderwarning', 'norecipientwarning', 'nosubj 'selectimportfile', 'messageissent', 'loadingdata', 'nopubkeyfor', 'nopubkeyforsender', 'encryptnoattachments','encryptedsendialog','searchpubkeyservers', 'importpubkeys', 'encryptpubkeysfound', 'search', 'close', 'import', 'keyid', 'keylength', 'keyexpired', - 'keyrevoked', 'keyimportsuccess', 'keyservererror', 'attaching', 'namex', 'attachmentrename'); + 'keyrevoked', 'keyimportsuccess', 'keyservererror', 'attaching', 'namex', 'attachmentrename', + 'disclosedrecipwarning', 'disclosedreciptitle', 'bccinstead', 'nosubjecttitle'); $OUTPUT->set_pagetitle($RCMAIL->gettext('compose')); @@ -99,6 +100,7 @@ $OUTPUT->set_env('top_posting', intval($RCMAIL->config->get('reply_mode')) > 0); $OUTPUT->set_env('sig_below', $RCMAIL->config->get('sig_below')); $OUTPUT->set_env('recipients_separator', trim($RCMAIL->config->get('recipients_separator', ','))); $OUTPUT->set_env('save_localstorage', (bool)$RCMAIL->config->get('compose_save_localstorage')); +$OUTPUT->set_env('max_disclosed_recipients', (int) $RCMAIL->config->get('max_disclosed_recipients', 5)); $OUTPUT->set_env('is_sent', false); $OUTPUT->set_env('mimetypes', rcmail_supported_mimetypes()); diff --git a/program/steps/mail/sendmail.inc b/program/steps/mail/sendmail.inc index 56f5c53d4..7cf1d94b7 100644 --- a/program/steps/mail/sendmail.inc +++ b/program/steps/mail/sendmail.inc @@ -44,9 +44,7 @@ if (!isset($COMPOSE['id'])) { } if (!$savedraft) { - if (empty($_POST['_to']) && empty($_POST['_cc']) && empty($_POST['_bcc']) - && empty($_POST['_subject']) && $_POST['_message'] - ) { + if (empty($_POST['_to']) && empty($_POST['_cc']) && empty($_POST['_bcc']) && $_POST['_message']) { $OUTPUT->show_message('sendingfailed', 'error'); $OUTPUT->send('iframe'); } @@ -171,10 +169,11 @@ if (($max_recipients = (int) $RCMAIL->config->get('max_recipients')) > 0) { $dont_override = (array) $RCMAIL->config->get('dont_override'); $mdn_enabled = in_array('mdn_default', $dont_override) ? $RCMAIL->config->get('mdn_default') : !empty($_POST['_mdn']); $dsn_enabled = in_array('dsn_default', $dont_override) ? $RCMAIL->config->get('dsn_default') : !empty($_POST['_dsn']); +$subject = trim(rcube_utils::get_input_value('_subject', rcube_utils::INPUT_POST, TRUE, $message_charset)); -// add subject -$headers['Subject'] = trim(rcube_utils::get_input_value('_subject', rcube_utils::INPUT_POST, TRUE, $message_charset)); - +if (strlen($subject)) { + $headers['Subject'] = $subject; +} if (!empty($identity_arr['organization'])) { $headers['Organization'] = $identity_arr['organization']; }