Skip to content

Commit

Permalink
webmail: top-post with no text selected and add "on ... wrote"-line, …
Browse files Browse the repository at this point in the history
…keep bottom-quoting with text selected

top-posting causes "On $datetime, $sender wrote:" above the quoted text to be
added (unless there was no Date header or valid address in a From header).

in the near future we should create settings, and add a setting for adding the
"on ... wrote"-line, ideally including a template.

for issue #83 by mattfbacon, thanks!
  • Loading branch information
mjl- committed Oct 13, 2023
1 parent 7d28d80 commit 8640fd8
Show file tree
Hide file tree
Showing 2 changed files with 33 additions and 4 deletions.
19 changes: 17 additions & 2 deletions webmail/webmail.js
Original file line number Diff line number Diff line change
Expand Up @@ -2145,7 +2145,7 @@ const compose = (opts) => {
maxWidth: '70em',
width: '40%',
borderRadius: '.25em',
}), dom.form(fieldset = dom.fieldset(dom.table(style({ width: '100%' }), dom.tr(dom.td(style({ textAlign: 'right', color: '#555' }), dom.span('From:')), dom.td(dom.clickbutton('Cancel', style({ float: 'right' }), attr.title('Close window, discarding message.'), clickCmd(cmdCancel, shortcuts)), from = dom.select(attr.required(''), style({ width: 'auto' }), fromOptions), ' ', toBtn = dom.clickbutton('To', clickCmd(cmdAddTo, shortcuts)), ' ', ccBtn = dom.clickbutton('Cc', clickCmd(cmdAddCc, shortcuts)), ' ', bccBtn = dom.clickbutton('Bcc', clickCmd(cmdAddBcc, shortcuts)), ' ', replyToBtn = dom.clickbutton('ReplyTo', clickCmd(cmdReplyTo, shortcuts)), ' ', customFromBtn = dom.clickbutton('From', attr.title('Set custom From address/name.'), clickCmd(cmdCustomFrom, shortcuts)))), toRow = dom.tr(dom.td('To:', style({ textAlign: 'right', color: '#555' })), toCell = dom.td(style({ width: '100%' }))), replyToRow = dom.tr(dom.td('Reply-To:', style({ textAlign: 'right', color: '#555' })), replyToCell = dom.td(style({ width: '100%' }))), ccRow = dom.tr(dom.td('Cc:', style({ textAlign: 'right', color: '#555' })), ccCell = dom.td(style({ width: '100%' }))), bccRow = dom.tr(dom.td('Bcc:', style({ textAlign: 'right', color: '#555' })), bccCell = dom.td(style({ width: '100%' }))), dom.tr(dom.td('Subject:', style({ textAlign: 'right', color: '#555' })), dom.td(style({ width: '100%' }), subject = dom.input(focusPlaceholder('subject...'), attr.value(opts.subject || ''), attr.required(''), style({ width: '100%' }))))), body = dom.textarea(dom._class('mono'), attr.rows('15'), style({ width: '100%' }), opts.body || '', opts.body && !opts.isForward ? prop({ selectionStart: opts.body.length, selectionEnd: opts.body.length }) : [], function keyup(e) {
}), dom.form(fieldset = dom.fieldset(dom.table(style({ width: '100%' }), dom.tr(dom.td(style({ textAlign: 'right', color: '#555' }), dom.span('From:')), dom.td(dom.clickbutton('Cancel', style({ float: 'right' }), attr.title('Close window, discarding message.'), clickCmd(cmdCancel, shortcuts)), from = dom.select(attr.required(''), style({ width: 'auto' }), fromOptions), ' ', toBtn = dom.clickbutton('To', clickCmd(cmdAddTo, shortcuts)), ' ', ccBtn = dom.clickbutton('Cc', clickCmd(cmdAddCc, shortcuts)), ' ', bccBtn = dom.clickbutton('Bcc', clickCmd(cmdAddBcc, shortcuts)), ' ', replyToBtn = dom.clickbutton('ReplyTo', clickCmd(cmdReplyTo, shortcuts)), ' ', customFromBtn = dom.clickbutton('From', attr.title('Set custom From address/name.'), clickCmd(cmdCustomFrom, shortcuts)))), toRow = dom.tr(dom.td('To:', style({ textAlign: 'right', color: '#555' })), toCell = dom.td(style({ width: '100%' }))), replyToRow = dom.tr(dom.td('Reply-To:', style({ textAlign: 'right', color: '#555' })), replyToCell = dom.td(style({ width: '100%' }))), ccRow = dom.tr(dom.td('Cc:', style({ textAlign: 'right', color: '#555' })), ccCell = dom.td(style({ width: '100%' }))), bccRow = dom.tr(dom.td('Bcc:', style({ textAlign: 'right', color: '#555' })), bccCell = dom.td(style({ width: '100%' }))), dom.tr(dom.td('Subject:', style({ textAlign: 'right', color: '#555' })), dom.td(style({ width: '100%' }), subject = dom.input(focusPlaceholder('subject...'), attr.value(opts.subject || ''), attr.required(''), style({ width: '100%' }))))), body = dom.textarea(dom._class('mono'), attr.rows('15'), style({ width: '100%' }), opts.body || '', opts.body && !opts.isForward && !opts.body.startsWith('\n\n') ? prop({ selectionStart: opts.body.length, selectionEnd: opts.body.length }) : [], function keyup(e) {
if (e.key === 'Enter') {
checkAttachments();
}
Expand Down Expand Up @@ -2571,8 +2571,10 @@ const newMsgView = (miv, msglistView, listMailboxes, possibleLabels, messageLoad
const pm = await parsedMessagePromise;
let body = '';
const sel = window.getSelection();
let haveSel = false;
if (sel && sel.toString()) {
body = sel.toString();
haveSel = true;
}
else if (pm.Texts && pm.Texts.length > 0) {
body = pm.Texts[0];
Expand All @@ -2582,7 +2584,20 @@ const newMsgView = (miv, msglistView, listMailboxes, possibleLabels, messageLoad
body = '\n\n---- Forwarded Message ----\n\n' + body;
}
else {
body = body.split('\n').map(line => '> ' + line).join('\n') + '\n\n';
body = body.split('\n').map(line => '> ' + line).join('\n');
if (haveSel) {
body += '\n\n';
}
else {
let onWroteLine = '';
if (mi.Envelope.Date && mi.Envelope.From && mi.Envelope.From.length === 1) {
const from = mi.Envelope.From[0];
const name = from.Name || formatEmailAddress(from);
const datetime = mi.Envelope.Date.toLocaleDateString(undefined, { weekday: "short", year: "numeric", month: "short", day: "numeric" }) + ' at ' + mi.Envelope.Date.toLocaleTimeString();
onWroteLine = 'On ' + datetime + ', ' + name + ' wrote:\n';
}
body = '\n\n' + onWroteLine + body;
}
}
const subjectPrefix = forward ? 'Fwd:' : 'Re:';
let subject = mi.Envelope.Subject || '';
Expand Down
18 changes: 16 additions & 2 deletions webmail/webmail.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1428,7 +1428,7 @@ const compose = (opts: ComposeOptions) => {
),
body=dom.textarea(dom._class('mono'), attr.rows('15'), style({width: '100%'}),
opts.body || '',
opts.body && !opts.isForward ? prop({selectionStart: opts.body.length, selectionEnd: opts.body.length}) : [],
opts.body && !opts.isForward && !opts.body.startsWith('\n\n') ? prop({selectionStart: opts.body.length, selectionEnd: opts.body.length}) : [],
function keyup(e: KeyboardEvent) {
if (e.key === 'Enter') {
checkAttachments()
Expand Down Expand Up @@ -2037,16 +2037,30 @@ const newMsgView = (miv: MsgitemView, msglistView: MsglistView, listMailboxes: l
const pm = await parsedMessagePromise
let body = ''
const sel = window.getSelection()
let haveSel = false
if (sel && sel.toString()) {
body = sel.toString()
haveSel = true
} else if (pm.Texts && pm.Texts.length > 0) {
body = pm.Texts[0]
}
body = body.replace(/\r/g, '').replace(/\n\n\n\n*/g, '\n\n').trim()
if (forward) {
body = '\n\n---- Forwarded Message ----\n\n'+body
} else {
body = body.split('\n').map(line => '> ' + line).join('\n') + '\n\n'
body = body.split('\n').map(line => '> ' + line).join('\n')
if (haveSel) {
body += '\n\n'
} else {
let onWroteLine = ''
if (mi.Envelope.Date && mi.Envelope.From && mi.Envelope.From.length === 1) {

This comment has been minimized.

Copy link
@mattfbacon

mattfbacon Oct 13, 2023

Contributor

Wait, an email can have multiple senders?

This comment has been minimized.

Copy link
@mjl-

mjl- Oct 13, 2023

Author Owner

sort of. in theory it can. the "from" header is just like "to" and "cc" etc. but in practice you would run into trouble if you tried to specify multiple addresses. dmarc really wants to see a single from address, it is all about verifying the (singular) domain in the message From is valid/allowed.

btw, the long line is what typescript generates. i haven't found an option yet to keep statements on multiple lines like in the original typescript. (i also haven't found a way to turn off the needless semicolons in the typescript output).

This comment has been minimized.

Copy link
@mattfbacon

mattfbacon Oct 13, 2023

Contributor

Eh, I would just run a formatter on the output rather than trying to fight with tsc.

This comment has been minimized.

Copy link
@mjl-

mjl- Oct 13, 2023

Author Owner

that would work, but i'm not a fan of the js ecosystem. most packages pull in the whole world of ever-updating npm dependencies. and i can live with the long lines and semicolons. the js is still mostly understandable in case of errors, and easy to map back to the typescript.

This comment has been minimized.

Copy link
@mattfbacon

mattfbacon Oct 13, 2023

Contributor

Well, bun (zig) can compile typescript to JS and it looks like https://github.com/web-infra-dev/oxc (Rust) can do JS formatting if you're not satisfied with the knobs that bun exposes.

This comment has been minimized.

Copy link
@mattfbacon

mattfbacon Oct 13, 2023

Contributor

Nevermind about bun, it doesn't actually check the types. Testing the formatter now.

This comment has been minimized.

Copy link
@mattfbacon

mattfbacon Oct 13, 2023

Contributor

Doesn't work either. But yeah I agree there's not really an issue with the JS, I just wasn't paying attention and thought it was the actual code.

const from = mi.Envelope.From[0]
const name = from.Name || formatEmailAddress(from)
const datetime = mi.Envelope.Date.toLocaleDateString(undefined, {weekday: "short", year: "numeric", month: "short", day: "numeric"}) + ' at ' + mi.Envelope.Date.toLocaleTimeString()
onWroteLine = 'On ' + datetime + ', ' + name + ' wrote:\n'
}
body = '\n\n' + onWroteLine + body
}
}
const subjectPrefix = forward ? 'Fwd:' : 'Re:'
let subject = mi.Envelope.Subject || ''
Expand Down

0 comments on commit 8640fd8

Please sign in to comment.