feat(mail(web)): Templates folder for pre-defined drafts

To create a template, the user needs to:

 - define a mailbox as the Templates folder;
 - move a draft to this special folder.

Fixes #4320
Fixes #5363
This commit is contained in:
Francis Lachapelle
2022-02-11 10:06:57 -05:00
parent 02b0a41567
commit c986422a3d
25 changed files with 230 additions and 9 deletions
+1
View File
@@ -24,6 +24,7 @@ Mailer_OBJC_FILES += \
SOGoDraftsFolder.m \
SOGoTrashFolder.m \
SOGoJunkFolder.m \
SOGoTemplatesFolder.m \
\
SOGoMailBodyPart.m \
SOGoHTMLMailBodyPart.m \
+4
View File
@@ -44,6 +44,7 @@
@class SOGoSentFolder;
@class SOGoTrashFolder;
@class SOGoJunkFolder;
@class SOGoTemplatesFolder;
typedef enum {
undefined = -1,
@@ -63,6 +64,7 @@ typedef enum {
SOGoSentFolder *sentFolder;
SOGoTrashFolder *trashFolder;
SOGoJunkFolder *junkFolder;
SOGoTemplatesFolder *templatesFolder;
SOGoIMAPAclStyle imapAclStyle;
NSMutableArray *identities;
NSString *otherUsersFolderName;
@@ -117,12 +119,14 @@ typedef enum {
- (NSString *) otherUsersFolderNameInContext: (id)_ctx;
- (NSString *) sharedFoldersNameInContext: (id)_ctx;
- (NSString *) junkFolderNameInContext: (id)_ctx;
- (NSString *) templatesFolderNameInContext: (id)_ctx;
- (SOGoMailFolder *) inboxFolderInContext: (id)_ctx;
- (SOGoDraftsFolder *) draftsFolderInContext: (id)_ctx;
- (SOGoSentFolder *) sentFolderInContext: (id)_ctx;
- (SOGoTrashFolder *) trashFolderInContext: (id)_ctx;
- (SOGoJunkFolder *) junkFolderInContext: (id)_ctx;
- (SOGoTemplatesFolder *) templatesFolderInContext: (id)_ctx;
/* namespaces */
+33
View File
@@ -48,6 +48,7 @@
#import "SOGoSentFolder.h"
#import "SOGoTrashFolder.h"
#import "SOGoJunkFolder.h"
#import "SOGoTemplatesFolder.h"
#import "SOGoUser+Mailer.h"
#import "SOGoMailAccount.h"
@@ -70,6 +71,7 @@ static NSString *inboxFolderName = @"INBOX";
sentFolder = nil;
trashFolder = nil;
junkFolder = nil;
templatesFolder = nil;
imapAclStyle = undefined;
identities = nil;
otherUsersFolderName = nil;
@@ -88,6 +90,7 @@ static NSString *inboxFolderName = @"INBOX";
[sentFolder release];
[trashFolder release];
[junkFolder release];
[templatesFolder release];
[identities release];
[otherUsersFolderName release];
[sharedFoldersName release];
@@ -100,6 +103,11 @@ static NSString *inboxFolderName = @"INBOX";
return NO;
}
- (BOOL) isInTemplatesFolder
{
return NO;
}
- (void) _appendNamespace: (NSArray *) namespace
toFolders: (NSMutableArray *) folders
{
@@ -406,6 +414,7 @@ static NSString *inboxFolderName = @"INBOX";
[self sentFolderNameInContext: context],
[self trashFolderNameInContext: context],
[self junkFolderNameInContext: context],
[self templatesFolderNameInContext: context],
nil] stringsWithFormat: @"/%@"];
folders = [[self imap4Connection] allFoldersForURL: [self imap4URL]
onlySubscribedFolders: subscribedOnly];
@@ -479,6 +488,9 @@ static NSString *inboxFolderName = @"INBOX";
else if ([flags containsObject: [self junkFolderNameInContext: context]] ||
[folderName isEqualToString: [self junkFolderNameInContext: context]])
folderType = @"junk";
else if ([flags containsObject: [self templatesFolderNameInContext: context]] ||
[folderName isEqualToString: [self templatesFolderNameInContext: context]])
folderType = @"templates";
else if ([folderName isEqualToString: otherUsersFolderName])
folderType = @"otherUsers";
else if ([folderName isEqualToString: sharedFoldersName])
@@ -1012,6 +1024,9 @@ static NSString *inboxFolderName = @"INBOX";
else if ([folderName
isEqualToString: [self junkFolderNameInContext: _ctx]])
klazz = [SOGoJunkFolder class];
else if ([folderName
isEqualToString: [self templatesFolderNameInContext: _ctx]])
klazz = [SOGoTemplatesFolder class];
else
klazz = [SOGoMailFolder class];
@@ -1081,6 +1096,11 @@ static NSString *inboxFolderName = @"INBOX";
return [self _userFolderNameWithPurpose: @"Junk"];
}
- (NSString *) templatesFolderNameInContext: (id)_ctx
{
return [self _userFolderNameWithPurpose: @"Templates"];
}
- (NSString *) otherUsersFolderNameInContext: (id)_ctx
{
return otherUsersFolderName;
@@ -1192,6 +1212,19 @@ static NSString *inboxFolderName = @"INBOX";
return junkFolder;
}
- (SOGoTemplatesFolder *) templatesFolderInContext: (id) _ctx
{
if (!templatesFolder)
{
templatesFolder
= [self folderWithTraversal: [self templatesFolderNameInContext: _ctx]
andClassName: @"SOGoTemplatesFolder"];
[templatesFolder retain];
}
return templatesFolder;
}
/* account delegation */
- (NSArray *) delegates
{
+1
View File
@@ -62,6 +62,7 @@
- (SOGoMailAccount *) mailAccountFolder;
- (SOGoMailAccounts *) mailAccountsFolder;
- (BOOL) isInDraftsFolder;
- (BOOL) isInTemplatesFolder;
/* IMAP4 */
+5
View File
@@ -121,6 +121,11 @@
return [container isInDraftsFolder];
}
- (BOOL) isInTemplatesFolder
{
return [container isInTemplatesFolder];
}
/* IMAP4 */
- (NGImap4ConnectionManager *) mailManager
+4
View File
@@ -1144,6 +1144,10 @@ static NSInteger _compareFetchResultsByUID (id entry1, id entry2, NSArray *uids)
isEqualToString:
[mailAccount junkFolderNameInContext: _ctx]])
className = @"SOGoJunkFolder";
else if ([fullFolderName
isEqualToString:
[mailAccount templatesFolderNameInContext: _ctx]])
className = @"SOGoTemplatesFolder";
/* else if ([folderName isEqualToString:
[mailAccount sieveFolderNameInContext: _ctx]])
obj = [self lookupFiltersFolder: _key inContext: _ctx]; */
+32
View File
@@ -0,0 +1,32 @@
/*
Copyright (C) 2022 Inverse inc.
This file is part of SOGo.
SOGo is free software; you can redistribute it and/or modify it under
the terms of the GNU Lesser General Public License as published by the
Free Software Foundation; either version 2, or (at your option) any
later version.
SOGo is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or
FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
License for more details.
You should have received a copy of the GNU Lesser General Public
License along with OGo; see the file COPYING. If not, write to the
Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA
02111-1307, USA.
*/
#ifndef __Mailer_SOGoTemplatesFolder_H__
#define __Mailer_SOGoTemplatesFolder_H__
#import "SOGoDraftsFolder.h"
@class SOGoDraftsFolder;
@interface SOGoTemplatesFolder : SOGoDraftsFolder
@end
#endif /* __Mailer_SOGoTemplatesFolder_H__ */
+31
View File
@@ -0,0 +1,31 @@
/*
Copyright (C) 2022 Inverse inc.
This file is part of SOGo.
SOGo is free software; you can redistribute it and/or modify it under
the terms of the GNU Lesser General Public License as published by the
Free Software Foundation; either version 2, or (at your option) any
later version.
SOGo is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or
FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
License for more details.
You should have received a copy of the GNU Lesser General Public
License along with OGo; see the file COPYING. If not, write to the
Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA
02111-1307, USA.
*/
#import "SOGoTemplatesFolder.h"
@implementation SOGoTemplatesFolder
- (BOOL) isInTemplatesFolder
{
return YES;
}
@end /* SOGoTemplatesFolder */
+1
View File
@@ -115,6 +115,7 @@
SOGoDraftsFolderName = "Drafts";
SOGoTrashFolderName = "Trash";
SOGoJunkFolderName = "Junk";
SOGoTemplatesFolderName = "Templates";
SOGoMailComposeMessageType = "html";
SOGoMailComposeFontSize = "0";
SOGoMailDisplayRemoteInlineImages = "never";
+2
View File
@@ -923,6 +923,8 @@
forKey: @"Trash"];
[mailboxes setObject: [_defaults junkFolderName]
forKey: @"Junk"];
[mailboxes setObject: [_defaults templatesFolderName]
forKey: @"Templates"];
[mailAccount setObject: mailboxes forKey: @"specialMailboxes"];
[mailboxes release];
+3
View File
@@ -111,6 +111,9 @@ extern NSString *SOGoWeekStartFirstFullWeek;
- (void) setJunkFolderName: (NSString *) newValue;
- (NSString *) junkFolderName;
- (void) setTemplatesFolderName: (NSString *) newValue;
- (NSString *) templatesFolderName;
- (void) setFirstDayOfWeek: (int) newValue;
- (int) firstDayOfWeek;
+11
View File
@@ -492,6 +492,17 @@ NSString *SOGoWeekStartFirstFullWeek = @"FirstFullWeek";
stringByEncodingImap4FolderName];
}
- (void) setTemplatesFolderName: (NSString *) newValue
{
[self setObject: newValue forKey: @"SOGoTemplatesFolderName"];
}
- (NSString *) templatesFolderName
{
return [[self stringForKey: @"SOGoTemplatesFolderName"]
stringByEncodingImap4FolderName];
}
- (void) setFirstDayOfWeek: (int) newValue
{
[self setInteger: newValue forKey: @"SOGoFirstDayOfWeek"];
@@ -187,6 +187,7 @@
"InboxFolderName" = "Inbox";
"DraftsFolderName" = "Drafts";
"JunkFolderName" = "Junk";
"TemplatesFolderName" = "Templates";
"SieveFolderName" = "Filters";
"Folders" = "Folders"; /* title line */
@@ -432,6 +433,9 @@
"Set as Sent" = "Set as Sent";
"Set as Trash" = "Set as Trash";
/* Set the folder as the one holding the Templates */
"Set as Templates" = "Set as Templates";
/* Set the folder as the one holding Junk mails */
"Set as Junk" = "Set as Junk";
@@ -454,6 +458,9 @@
"Your email has been sent" = "Your email has been sent";
"Folder compacted" = "Folder compacted";
"New draft" = "New draft";
"Create new draft with this template" = "Create new draft with this template";
/* Advanced search */
"Enter Subject" = "Enter Subject";
"Enter From" = "Enter From";
+30
View File
@@ -118,6 +118,36 @@
andString: [data jsonRepresentation]];
}
- (WOResponse *) composeAction
{
SOGoMailAccount *account;
SOGoMailObject *co;
SOGoDraftsFolder *drafts;
SOGoDraftObject *newMail;
NSString *accountName, *mailboxName, *messageName;
NSDictionary *data;
co = [self clientObject];
account = [co mailAccountFolder];
drafts = [account draftsFolderInContext: context];
newMail = [drafts newDraft];
[newMail fetchMailForEditing: co];
[newMail storeInfo];
accountName = [account nameInContainer];
mailboxName = [drafts absoluteImap4Name];
mailboxName = [mailboxName substringWithRange: NSMakeRange(1, [mailboxName length] -2)];
messageName = [newMail nameInContainer];
data = [NSDictionary dictionaryWithObjectsAndKeys:
accountName, @"accountId",
mailboxName, @"mailboxPath",
messageName, @"draftId", nil];
return [self responseWithStatus: 201
andString: [data jsonRepresentation]];
}
- (WOResponse *) viewPlainAction
{
BOOL htmlContent;
+7
View File
@@ -68,6 +68,7 @@
}
- (BOOL) mailIsDraft;
- (BOOL) mailIsTemplate;
- (NSNumber *) shouldAskReceipt;
- (NSString *) formattedDate;
- (NSString *) _matchingIdentityEMailOrDefault: (BOOL) useDefault;
@@ -340,6 +341,7 @@ static NSString *mailETag = nil;
data = [NSMutableDictionary dictionaryWithObjectsAndKeys:
[self shouldAskReceipt], @"shouldAskReceipt",
[NSNumber numberWithBool: [self mailIsDraft]], @"isDraft",
[NSNumber numberWithBool: [self mailIsTemplate]], @"isTemplate",
renderedPart, @"parts",
nil];
if ([self formattedDate])
@@ -803,6 +805,11 @@ static NSString *mailETag = nil;
return [[self clientObject] isInDraftsFolder];
}
- (BOOL) mailIsTemplate
{
return [[self clientObject] isInTemplatesFolder];
}
- (id) redirectToParentFolder
{
id url;
+10
View File
@@ -119,6 +119,11 @@
actionClass = "UIxMailFolderActions";
actionName = "setAsJunkFolder";
};
setAsTemplatesFolder = {
protectedBy = "View";
actionClass = "UIxMailFolderActions";
actionName = "setAsTemplatesFolder";
};
markMessagesAsJunk = {
protectedBy = "View";
actionClass = "UIxMailFolderActions";
@@ -237,6 +242,11 @@
actionClass = "UIxMailActions";
actionName = "edit";
};
compose = {
protectedBy = "View";
actionClass = "UIxMailActions";
actionName = "compose";
};
reply = {
protectedBy = "View";
actionClass = "UIxMailActions";
+3 -1
View File
@@ -60,7 +60,9 @@
<div ng-message="required">This field is required</div>
</div>
</md-autocomplete>
<md-button class="sg-icon-button" ng-click="editor.send()"
<md-button class="sg-icon-button"
ng-click="editor.send()"
ng-hide="editor.saveAsTemplate"
ng-disabled="!(editor.message.editable.to.length > 0 || editor.message.editable.cc.length > 0 || editor.message.editable.bcc.length > 0) || editor.uploader.isUploading || messageForm.$invalid || messageForm.$submitted"
sg-ripple-click="mailEditor">
<md-icon>send</md-icon>
@@ -274,6 +274,11 @@
<var:string label:value="Set as Junk"/>
</md-button>
</md-menu-item>
<md-menu-item ng-show="::$menuCtrl.folder.$canFolderAs()">
<md-button type="button" ng-click="$menuCtrl.setFolderAs('Templates')">
<var:string label:value="Set as Templates"/>
</md-button>
</md-menu-item>
<md-divider ng-show="::($menuCtrl.folder.type != 'additional')"><!-- divider --></md-divider>
<md-menu-item ng-show="::($menuCtrl.folder.type != 'additional')">
<md-button type="button" ng-click="$menuCtrl.share()">
+13 -2
View File
@@ -155,7 +155,7 @@
</div>
<div class="msg-recipients" layout="column" flex="50" flex-xs="100">
<!-- to -->
<div class="pseudo-input-container--compact" ng-show="::viewer.message.to.length > 0">
<div class="pseudo-input-container--compact" ng-show="viewer.message.to.length > 0">
<label class="pseudo-input-label">
<var:string label:value="To"/>
</label>
@@ -255,9 +255,20 @@
</md-chips>
</div>
<!-- New draft from template -->
<div class="hide-print" ng-show="viewer.message.isTemplate">
<md-divider><!-- divider --></md-divider>
<md-list-item class="md-clickable" layout="row" layout-align="space-between center"
label:aria-label="New draft"
ng-click="viewer.compose($event)">
<md-icon>drafts</md-icon>
<p><var:string label:value="Create new draft with this template"/></p>
</md-list-item>
</div>
<!-- S/MIME Signature -->
<sg-block-toggle class="hide-print" layout="column"
ng-show="::viewer.message.signed">
ng-show="viewer.message.signed">
<md-divider><!-- divider --></md-divider>
<md-list-item class="sg-button-toggle">
<div>
+4 -2
View File
@@ -134,8 +134,10 @@ String.prototype.formatted = function() {
return newString;
};
String.prototype.isValidEmail = function() {
var emailRE = /^([\w\!\#$\%\&\'\*\+\-\/\=\?\^\`{\|\}\~]+\.)*[\w\!\#$\%\&\'\*\+\-\/\=\?\^\`{\|\}\~]+@((((([a-z0-9]{1}[a-z0-9\-]{0,62}[a-z0-9]{1})|[a-z])\.)+[a-z]{2,})|(\d{1,3}\.){3}\d{1,3}(\:\d{1,5})?)$/i;
String.prototype.isValidEmail = function(strict) {
var emailRE = strict ?
/^([\w\!\#$\%\&\'\*\+\-\/\=\?\^\`{\|\}\~]+\.)*[\w\!\#$\%\&\'\*\+\-\/\=\?\^\`{\|\}\~]+@((((([a-z0-9]{1}[a-z0-9\-]{0,62}[a-z0-9]{1})|[a-z])\.)+[a-z]{2,})|(\d{1,3}\.){3}\d{1,3}(\:\d{1,5})?)$/i : /([\w\!\#$\%\&\'\*\+\-\/\=\?\^\`{\|\}\~]+\.)*[\w\!\#$\%\&\'\*\+\-\/\=\?\^\`{\|\}\~]+@((((([a-z0-9]{1}[a-z0-9\-]{0,62}[a-z0-9]{1})|[a-z])\.)+[a-z]{2,})|(\d{1,3}\.){3}\d{1,3}(\:\d{1,5})?)/i;
return emailRE.test(this);
};
@@ -203,6 +203,10 @@
this.$displayName = l('JunkFolderName');
this.$icon = 'thumb_down';
}
else if (this.type == 'templates') {
this.$displayName = l('TemplatesFolderName');
this.$icon = 'mail_outline';
}
else if (this.type == 'additional') {
this.$icon = 'folder';
}
@@ -776,10 +776,20 @@
return this.$newDraft('forward');
};
/**
* @function $compose
* @memberof Message.prototype
* @desc Prepare a new Message object as a new draft from a copy of this message.
* @returns a promise of the HTTP operations
*/
Message.prototype.$compose = function() {
return this.$newDraft('compose');
};
/**
* @function $newDraft
* @memberof Message.prototype
* @desc Prepare a new Message object as a reply or a forward of the current message and associated
* @desc Prepare a new Message object as a reply, a forward or a copy of the current message and associated
* to the draft mailbox.
* @see {@link Account.$newMessage}
* @see {@link Message.$editableContent}
@@ -479,6 +479,12 @@
}
};
this.compose = function($event) {
if (!this._showMailEditorInPopup('compose')) {
_showMailEditor($event, this.message.$compose());
}
};
this.openInPopup = function(action) {
var url = [sgSettings.baseURL(),
'UIxMailPopupView#!/Mail',
@@ -304,7 +304,6 @@
this.addRecipient = function (contact, field) {
var recipients, recipient, list, i, address;
var emailRE = /([\w\!\#$\%\&\'\*\+\-\/\=\?\^\`{\|\}\~]+\.)*[\w\!\#$\%\&\'\*\+\-\/\=\?\^\`{\|\}\~]+@((((([a-z0-9]{1}[a-z0-9\-]{0,62}[a-z0-9]{1})|[a-z])\.)+[a-z]{2,})|(\d{1,3}\.){3}\d{1,3}(\:\d{1,5})?)/i;
recipients = this.message.editable[field];
@@ -319,7 +318,7 @@
contact.charCodeAt(i) == 32 || // space
contact.charCodeAt(i) == 44 || // ,
contact.charCodeAt(i) == 59) && // ;
emailRE.test(address) &&
address.isValidEmail() &&
recipients.indexOf(address) < 0) {
recipients.push(address);
address = '';
@@ -33,7 +33,7 @@
// this.service = Message;
this.MailboxService = Mailbox;
if (Mailbox.selectedFolder.type == 'draft')
if (Mailbox.selectedFolder.type == 'draft' || Mailbox.selectedFolder.type == 'templates')
watchedAttrs.push('subject');
$scope.$watch(