Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/app/compose/compose.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ export class ComposeComponent implements AfterViewInit, OnDestroy, OnInit {
this.draftDeskservice.fromsSubject.value[0].nameAndAddress : '';
} else {
this.model.from = from.nameAndAddress;
if ( !this.has_pasted_signature && from.signature ) {
if ( !this.model.skipSignature && !this.has_pasted_signature && from.signature ) {
this.has_pasted_signature = true;
this.signature = from.signature;
// Sig is HTML, or Reply/Fwd Draft is HTML (contains .html)
Expand Down
63 changes: 62 additions & 1 deletion src/app/compose/draftdesk.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
// along with Runbox 7. If not, see <https://www.gnu.org/licenses/>.
// ---------- END RUNBOX LICENSE ----------

import { DraftFormModel, DraftDeskService } from './draftdesk.service';
import { DraftFormModel, DraftDeskService, ForwardedAttachment } from './draftdesk.service';
import { Identity } from '../profiles/profile.service';
import { MailAddressInfo } from '../common/mailaddressinfo';
import { RunboxWebmailAPI } from '../rmmapi/rbwebmail';
Expand Down Expand Up @@ -126,6 +126,67 @@ Subject: Test subject <br />
expect(draft.isUnsaved()).toBe(true);
done();
});
it('Send again: copies a sent HTML message into a new draft', (done) => {
const draft = DraftFormModel.sendAgain({
mid: 123,
from: [
{address: 'from@runbox.com', name: 'From'}
],
to: [
{address: 'to@runbox.com', name: 'To'}
],
cc: [
{address: 'cc@runbox.com', name: 'CC'}
],
bcc: [
{address: 'bcc@runbox.com', name: 'BCC'}
],
attachments: [
{cid: 'inline-image'}
],
subject: 'Test subject',
rawtext: 'blabla\nabcde',
sanitized_html: '<p>blabla</p><p>abcde</p>'
},
[
Identity.fromObject({'email':'fallback@runbox.com'}),
Identity.fromObject({'email':'from@runbox.com'})
]);

expect(draft.subject).toBe('Test subject');
expect(draft.from).toBe('from@runbox.com');
expect(draft.to[0].nameAndAddress).toBe('"To" <to@runbox.com>');
expect(draft.cc[0].nameAndAddress).toBe('"CC" <cc@runbox.com>');
expect(draft.bcc[0].nameAndAddress).toBe('"BCC" <bcc@runbox.com>');
expect(draft.msg_body).toBe('blabla\nabcde');
expect(draft.html).toBe('<p>blabla</p><p>abcde</p>');
expect(draft.useHTML).toBe(true);
expect(draft.skipSignature).toBe(true);
expect(draft.attachments[0] instanceof ForwardedAttachment).toBeTrue();
expect((draft.attachments[0] as ForwardedAttachment).messageId).toBe('123');
expect(draft.isUnsaved()).toBe(true);
done();
});
it('Send again: falls back to the default identity for a non-matching sender', (done) => {
const draft = DraftFormModel.sendAgain({
from: [
{address: 'old-address@runbox.com', name: 'Old'}
],
to: null,
attachments: [],
subject: 'Plain subject',
rawtext: 'plain body'
},
[ Identity.fromObject({'email':'default@runbox.com'}) ]);

expect(draft.subject).toBe('Plain subject');
expect(draft.from).toBe('default@runbox.com');
expect(draft.to).toEqual([]);
expect(draft.msg_body).toBe('plain body');
expect(draft.useHTML).toBe(false);
expect(draft.isUnsaved()).toBe(true);
done();
});
it('Reply: Address with object, single reply-to', (done) => {
console.log('Reply test: Address with object, single reply-to');
// fromObj, identities, all (t/f), html (t/f)
Expand Down
41 changes: 41 additions & 0 deletions src/app/compose/draftdesk.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ export class DraftFormModel {
in_reply_to: string;
reply_to_id: string = null;
replying = false;
skipSignature = false;
tags: string;
useHTML = false;
save = 'Save';
Expand Down Expand Up @@ -209,13 +210,53 @@ ${mailObj.sanitized_html}`;
return ret;
}

public static sendAgain(mailObj, froms: Identity[]): DraftFormModel {
const ret = new DraftFormModel();

if (froms.length > 0) {
const originalFrom = mailObj.from && mailObj.from[0] && mailObj.from[0].address
? mailObj.from[0].address.toLowerCase()
: null;
ret.from = (
froms.find(fromObj => fromObj.email.toLowerCase() === originalFrom) || froms[0]
).email;
} else {
console.error('DraftDesk: No froms passed to sendAgain');
}

ret.subject = mailObj.subject;
ret.skipSignature = true;
ret.to = DraftFormModel.copyAddresses(mailObj.to);
ret.cc = DraftFormModel.copyAddresses(mailObj.cc);
ret.bcc = DraftFormModel.copyAddresses(mailObj.bcc);
ret.msg_body = mailObj.rawtext || (mailObj.text && mailObj.text.text) || '';
ret.preview = DraftFormModel.trimmedPreview(ret.msg_body);

if (mailObj.sanitized_html) {
ret.html = mailObj.sanitized_html;
ret.useHTML = true;
}

ret.attachments = (mailObj.attachments || []).map((attachment, ndx) =>
new ForwardedAttachment(String(mailObj.mid), ndx, attachment.cid)
);

return ret;
}

public isUnsaved(): boolean {
if (this.mid <= -1) {
return true;
}
return false;
}

private static copyAddresses(addresses): MailAddressInfo[] {
return Array.isArray(addresses)
? addresses.map((addr) => new MailAddressInfo(addr.name, addr.address))
: [];
}

private setFromForResponse(mailObj, froms: Identity[]): void {
if (froms.length > 0) {
this.from = (
Expand Down
1 change: 1 addition & 0 deletions src/app/mailviewer/messageactions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export interface MessageActions {
reply(useHTML?: boolean);
replyToAll(useHTML?: boolean);
forward(useHTML?: boolean);
sendAgain();
markSeen(seen_flag_value?: number);
trainSpam(params: any);
blockSender(param: string)
Expand Down
4 changes: 4 additions & 0 deletions src/app/mailviewer/rmm6messageactions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ export class RMM6MessageActions implements MessageActions {
this.openCompose('/mail/forward?message=' + this.mailViewerComponent.messageId);
}

sendAgain() {
this.snackBar.open('Not supported in RMM6 yet', null, {duration: 1000});
}

markSeen(seen_flag_value = 1) {
this.http.put('/rest/v1/email/' + this.mailViewerComponent.messageId, JSON.stringify(
{ seen_flag : seen_flag_value }))
Expand Down
10 changes: 10 additions & 0 deletions src/app/mailviewer/rmm7messageactions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,16 @@ export class RMM7MessageActions implements MessageActions {
});
}

public sendAgain() {
ProgressDialog.open(this.dialog);
this.draftDeskService.newDraft(
DraftFormModel.sendAgain(this.mailViewerComponent.mailObj, this.draftDeskService.fromsSubject.value)
).then(() => {
this.mailViewerComponent.close('goToDraftDesk');
ProgressDialog.close();
});
}

public markSeen(seen_flag_value = 1) {
this.updateMessages({
messageIds: [this.mailViewerComponent.messageId],
Expand Down
37 changes: 22 additions & 15 deletions src/app/mailviewer/singlemailviewer.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,18 @@
<button *ngIf="morebuttonindex>4" mat-icon-button matTooltip="Forward" (click)="messageActionsHandler.forward(showHTML)">
<mat-icon svgIcon="forward"></mat-icon>
</button>
<ng-container *ngIf="morebuttonindex>5">
<button *ngIf="isSentFolder && morebuttonindex>5" mat-icon-button matTooltip="Send again" (click)="messageActionsHandler.sendAgain()">
<mat-icon svgIcon="send"></mat-icon>
</button>
<ng-container *ngIf="morebuttonindex>toolbarIndex(5)">
<button *ngIf="mailObj.seen_flag===1" mat-icon-button matTooltip="Mark unread" (click)="messageActionsHandler.markSeen(0)">
<mat-icon svgIcon="email-mark-as-unread"></mat-icon>
</button>
<button *ngIf="mailObj.seen_flag===0" mat-icon-button matTooltip="Mark read" (click)="messageActionsHandler.markSeen(1)">
<mat-icon svgIcon="email-open"></mat-icon>
</button>
</ng-container>
<ng-container *ngIf="morebuttonindex>6">
<ng-container *ngIf="morebuttonindex>toolbarIndex(6)">
<button mat-icon-button *ngIf="mailObj.flagged_flag===0" matTooltip="Flag" (click)="messageActionsHandler.flag()">
<mat-icon svgIcon="flag"></mat-icon>
</button>
Expand All @@ -55,29 +58,29 @@
</svg>
</button>
</ng-container>
<ng-container *ngIf="morebuttonindex>7">
<ng-container *ngIf="morebuttonindex>toolbarIndex(7)">
<button mat-icon-button *ngIf="folder!==this.messagelistservice.spamFolderName" matTooltip="Report spam" (click)="messageActionsHandler.trainSpam({is_spam:1})">
<mat-icon svgIcon="alert-octagon"></mat-icon>
</button>
<button mat-icon-button *ngIf="folder===this.messagelistservice.spamFolderName" matTooltip="Not spam" (click)="messageActionsHandler.trainSpam({is_spam:0})">
<mat-icon svgIcon="alert-octagon-outline"></mat-icon>
</button>
</ng-container>
<button mat-icon-button *ngIf="morebuttonindex>8" matTooltip="Block/Allow Sender" [matMenuTriggerFor]="senderRulesMenu">
<button mat-icon-button *ngIf="morebuttonindex>toolbarIndex(8)" matTooltip="Block/Allow Sender" [matMenuTriggerFor]="senderRulesMenu">
<mat-icon svgIcon="filter-cog"></mat-icon>
</button>
<a *ngIf="morebuttonindex>9" mat-icon-button
<a *ngIf="morebuttonindex>toolbarIndex(9)" mat-icon-button
matTooltip="View source" [href]="'/rest/v1/email/'+messageId+'/raw'" target="_blank">
<mat-icon svgIcon="code-tags"></mat-icon>
</a>
<button mat-icon-button *ngIf="mailContentHTML && morebuttonindex>10" matTooltip="HTML settings" [matMenuTriggerFor]="htmlSettingsMenu">
<button mat-icon-button *ngIf="mailContentHTML && morebuttonindex>toolbarIndex(10)" matTooltip="HTML settings" [matMenuTriggerFor]="htmlSettingsMenu">
<mat-icon svgIcon="cog"></mat-icon>
</button>
<a *ngIf="mailContentHTML && morebuttonindex>11" mat-icon-button
<a *ngIf="mailContentHTML && morebuttonindex>toolbarIndex(11)" mat-icon-button
matTooltip="Original HTML" [href]="'/rest/v1/email/'+messageId+'/html'" target="_blank">
<mat-icon svgIcon="alert"></mat-icon>
</a>
<button mat-icon-button *ngIf="morebuttonindex < 12" [matMenuTriggerFor]="moreMailActionsMenu" matTooltip="More message actions">
<button mat-icon-button *ngIf="morebuttonindex < toolbarIndex(12)" [matMenuTriggerFor]="moreMailActionsMenu" matTooltip="More message actions">
<mat-icon svgIcon="dots-vertical"></mat-icon>
</button>

Expand Down Expand Up @@ -148,11 +151,15 @@
<mat-icon svgIcon="forward"></mat-icon>
<span>Forward</span>
</button>
<button *ngIf="morebuttonindex<6" mat-menu-item (click)="messageActionsHandler.markSeen(0)">
<button *ngIf="isSentFolder && morebuttonindex<6" mat-menu-item (click)="messageActionsHandler.sendAgain()">
<mat-icon svgIcon="send"></mat-icon>
<span>Send again</span>
</button>
<button *ngIf="morebuttonindex<toolbarIndex(6)" mat-menu-item (click)="messageActionsHandler.markSeen(0)">
<mat-icon svgIcon="email-mark-as-unread"></mat-icon>
<span>Mark unread</span>
</button>
<ng-container *ngIf="morebuttonindex<7">
<ng-container *ngIf="morebuttonindex<toolbarIndex(7)">
<button mat-menu-item *ngIf="mailObj.flagged_flag===0" (click)="messageActionsHandler.flag()">
<mat-icon svgIcon="flag"></mat-icon>
<span>Flag</span>
Expand All @@ -162,7 +169,7 @@
<span>Remove flag</span>
</button>
</ng-container>
<ng-container *ngIf="morebuttonindex<8">
<ng-container *ngIf="morebuttonindex<toolbarIndex(8)">
<button mat-menu-item *ngIf="folder!=='Spam'" (click)="messageActionsHandler.trainSpam({is_spam:1})">
<mat-icon svgIcon="alert-octagon"></mat-icon>
<span>Report spam</span>
Expand All @@ -172,20 +179,20 @@
<span>Not spam</span>
</button>
</ng-container>
<button mat-menu-item *ngIf="morebuttonindex<9" [matMenuTriggerFor]="blockSenderMenu">
<button mat-menu-item *ngIf="morebuttonindex<toolbarIndex(9)" [matMenuTriggerFor]="blockSenderMenu">
<mat-icon svgIcon="block"></mat-icon>
<span>Block Sender/Domain</span>
</button>
<a *ngIf="morebuttonindex<9" mat-menu-item
<a *ngIf="morebuttonindex<toolbarIndex(10)" mat-menu-item
[href]="'/rest/v1/email/'+messageId+'/raw'" target="_blank">
<mat-icon svgIcon="code-tags"></mat-icon>
<span>View source</span>
</a>
<button mat-menu-item *ngIf="mailContentHTML && morebuttonindex<10" [matMenuTriggerFor]="htmlSettingsMenu">
<button mat-menu-item *ngIf="mailContentHTML && morebuttonindex<toolbarIndex(11)" [matMenuTriggerFor]="htmlSettingsMenu">
<mat-icon svgIcon="cog"></mat-icon>
<span>HTML settings</span>
</button>
<a *ngIf="mailContentHTML && morebuttonindex<11" mat-menu-item
<a *ngIf="mailContentHTML && morebuttonindex<toolbarIndex(12)" mat-menu-item
[href]="'/rest/v1/email/'+messageId+'/html'" target="_blank">
<mat-icon svgIcon="alert"></mat-icon>
<span>Original HTML</span>
Expand Down
20 changes: 18 additions & 2 deletions src/app/mailviewer/singlemailviewer.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { MatExpansionModule } from '@angular/material/expansion';
import { MatGridListModule } from '@angular/material/grid-list';
import { MatIcon, MatIconModule } from '@angular/material/icon';
import { MatIconTestingModule } from '@angular/material/icon/testing';
import { MatLegacyListModule as MatListModule } from '@angular/material/legacy-list';
import { MatLegacyMenuModule as MatMenuModule } from '@angular/material/legacy-menu';
import { MatLegacyRadioModule as MatRadioModule } from '@angular/material/legacy-radio';
import { MatToolbarModule } from '@angular/material/toolbar';
Expand Down Expand Up @@ -106,6 +107,7 @@ describe('SingleMailViewerComponent', () => {
MatIconModule,
MatIconTestingModule,
MatGridListModule,
MatListModule,
MatToolbarModule,
MatTooltipModule,
MatDividerModule,
Expand All @@ -117,7 +119,7 @@ describe('SingleMailViewerComponent', () => {
providers: [
MobileQueryService,
StorageService,
{ provide: MessageListService, useValue: { spamFolderName: 'Spam' }},
{ provide: MessageListService, useValue: { spamFolderName: 'Spam', trashFolderName: 'Trash' }},
{ provide: HttpClient, useValue: {} },
{ provide: PreferencesService, useValue: {
preferences: new ReplaySubject<Map<string, any>>(),
Expand All @@ -143,8 +145,17 @@ describe('SingleMailViewerComponent', () => {
'name': 'Testy' }]
},
date: new Date(2016, 0, 1).toJSON(),
subject: 'Test subject'
subject: 'Test subject',
to: {
value: [{ 'address': 'to@runbox.com',
'name': 'Recipient' }]
},
bcc: {
value: [{ 'address': 'bcc@runbox.com',
'name': 'Hidden Recipient' }]
}
},
folder: 'Sent.Subfolder',
text: {
text: 'blablabla',
html: null,
Expand Down Expand Up @@ -198,6 +209,9 @@ describe('SingleMailViewerComponent', () => {
forward(useHTML?: boolean) {
throw new Error('Method not implemented.');
}
sendAgain() {
throw new Error('Method not implemented.');
}
markSeen(seen_flag_value?: number) {
throw new Error('Method not implemented.');
}
Expand Down Expand Up @@ -227,6 +241,8 @@ describe('SingleMailViewerComponent', () => {

expect(component.mailObj.attachments[0].downloadURL.indexOf('attachment/0')).toBeGreaterThan(-1);
expect(component.mailObj.attachments[0].thumbnailURL.indexOf('attachmentimagethumbnail/0')).toBeGreaterThan(-1);
expect(component.mailObj.bcc[0].address).toBe('bcc@runbox.com');
expect(component.isSentFolder).toBe(true);

expect(component.mailObj.attachments[1].downloadURL.indexOf('blob:')).toBe(0);
}));
Expand Down
9 changes: 9 additions & 0 deletions src/app/mailviewer/singlemailviewer.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,14 @@ export class SingleMailViewerComponent implements OnInit, DoCheck, AfterViewInit

folder: string;

public get isSentFolder(): boolean {
return !!this.folder && this.folder.indexOf('Sent') === 0;
}

public toolbarIndex(index: number): number {
return this.isSentFolder ? index + 1 : index;
}


constructor(
private domSanitizer: DomSanitizer,
Expand Down Expand Up @@ -537,6 +545,7 @@ export class SingleMailViewerComponent implements OnInit, DoCheck, AfterViewInit
res.from = res.headers.from.value.map(f => new MailAddressInfo(f.name,f.address));
res.to = res.headers.to ? res.headers.to.value : '';
res.cc = res.headers.cc ? res.headers.cc.value : '';
res.bcc = res.headers.bcc ? res.headers.bcc.value : '';

// RFC 5322 says "Date" and "From" are the only 2 required fields
// and yet we get emails without em.
Expand Down