Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Browser notifications #284

Merged
merged 10 commits into from
Mar 28, 2020
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ For convenient use with Grunt, try [grunt-maildev](https://github.com/xavierprio
--web-ip <ip address> IP Address to bind HTTP service to, defaults to --ip
--web-user <user> HTTP user for GUI
--web-pass <password> HTTP password for GUI
--https Switch from http to https protocol
--https-key <file> The file path to the ssl private key
--https-cert <file> The file path to the ssl cert file
--base-pathname <path> base path for URLs
--disable-web Disable the use of the web interface. Useful for unit testing
--hide-extensions <extensions> Comma separated list of SMTP extensions to NOT advertise
Expand Down
12 changes: 10 additions & 2 deletions app/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,16 @@
<i class="fa fa-envelope a"></i>
MailDev
</a>
<div class="autoshow-toggle">
<i class="fa" ng-class="{'fa-bell' : autoShow, 'fa-bell-o': !autoShow}" ng-click="toggleAutoShow()" title="Automatically show new emails as they arrive"></i>
<div class="options-toggle">
<i class="option fa"
ng-if="notificationsSupported"
ng-class="{'fa-comment disabled' : webNotifications, 'fa-comment-o': !webNotifications}"
ng-click="enableNotifications()"
title="Show browser notification when emails arrive"></i>
<i class="option fa"
ng-class="{'fa-bell' : autoShow, 'fa-bell-o': !autoShow}"
ng-click="toggleAutoShow()"
title="Automatically show new emails as they arrive"></i>
</div>
</div>
<div>
Expand Down
38 changes: 37 additions & 1 deletion app/scripts/controllers/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,17 @@
/**
* Main App Controller -- Manage all emails visible in the list
*/
var refreshTimeout = null
var notificationTimeout = null

app.controller('MainCtrl', [
'$scope', '$rootScope', '$http', 'Email', '$route', '$location', 'Favicon',
function ($scope, $rootScope, $http, Email, $route, $location, Favicon) {
$scope.items = []
$scope.configOpen = false
$scope.currentItemId = null
$scope.notificationsSupported = 'Notification' in window
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should give user feedback if it's supported also using isSecureContext.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting, what kind of feedback are you thinking of?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I decided to add a title attribute on the Notifications toggle now. Also with the new toggle design, Notifications is now greyed out if the browser context does not support it:

Screen Shot 2020-03-30 at 9 18 57 AM

$scope.webNotifications = window.Notification && window.Notification.permission === 'granted'
$scope.autoShow = false
$scope.unreadItems = 0

Expand Down Expand Up @@ -38,7 +42,6 @@ app.controller('MainCtrl', [
}
})

var refreshTimeout = null
$rootScope.$on('newMail', function (e, newEmail) {
// update model
$scope.items.push(newEmail)
Expand All @@ -54,6 +57,18 @@ app.controller('MainCtrl', [
$scope.$apply()
}, 200)
}

// show notifications
if (!notificationTimeout && $scope.webNotifications) {
notificationTimeout = setTimeout(function () {
notificationTimeout = null
}, 2000)
new window.Notification('MailDev', { body: newEmail.subject, icon: 'favicon.ico' })
.addEventListener('click', function () {
$location.path('/email/' + newEmail.id)
$scope.$apply()
})
}
})

$rootScope.$on('deleteMail', function (e, email) {
Expand Down Expand Up @@ -93,6 +108,27 @@ app.controller('MainCtrl', [
$scope.autoShow = !$scope.autoShow
}

$scope.enableNotifications = function () {
if (window.Notification && window.Notification.permission === 'granted') {
window.alert('To disable notifications, revoke the permissions in your browser.')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of this alert, can we instead set $scope.webNotifications = false? This context will of course be lost when the browser window is closed, but it might be a better workflow. Perhaps if this was coupled with some setting storage in localStorage that may provide the best experience.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting, hadn't thought about that. So instead of revoking permissions, you just silence the notifications. Great idea. The silence setting has to be stored preferrably. I as user found it already annoying to toggle the "autoShow" each time.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the input - all settings will be persisted after #291 :)

return
}
window.Notification.requestPermission().then(function (permissions) {
$scope.webNotifications = permissions === 'granted'
}).catch(function () {
window.alert('Unable to enable web notifications. See console for more information')
})
if (!window.isSecureContext && window.console) {
console.info(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rather make this more obvious to the user, perhaps using window.alert or a title on the html element.

'Web notifications can only be enabled on websites with https.\n\n' +
'You can enable https for MailDev with self-signed certificate. See `docs/https.md`\n\n' +
'For Firefox you can circumvent this restriction temporarily:\n' +
'In the address bar type `about:config`, and toggle `dom.webnotifications.allowinsecure` \n' +
'Don\'t forget to reset it again after enabling notifications in MailDev'
)
}
}

// Initialize the view
loadData()

Expand Down
2 changes: 1 addition & 1 deletion app/styles/style.css

Large diffs are not rendered by default.

16 changes: 13 additions & 3 deletions assets/styles/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -384,9 +384,19 @@ code {
background: $brandLighter;
}

.autoshow-toggle {
.options-toggle {
float: right;
cursor: pointer;
color: white;
padding-top: 3px;

> .option {
color: white;
cursor: pointer;
+ .option {
margin-left: 5px;
}

&.disabled {
cursor: default;
}
}
}
32 changes: 32 additions & 0 deletions docs/https.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# HTTPS
By default MailDev will run on the http protocol. For Web Notification support you'll need
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One can also use web notification support on localhost in most browsers. We should update this copy.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah. I wasn't aware localhost was the exception. Indeed useful to be added.

to run MailDev on https. This can be done with a self signed certificate.

## Create certificate
Generate the certificate:
```shell script
openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /etc/ssl/private/maildev.key -out /etc/ssl/certs/maildev.crt
```

Answer the FQDN question with your domain name or ip:
```shell script
Common Name (e.g. server FQDN or YOUR name) []: 192.168.1.103
```

Files created:
- /etc/ssl/private/maildev.key
- /etc/ssl/certs/maildev.crt

## Start MailDev with https
Add the following arguments to your MailDev startup:
```shell script
--https
--https-key /etc/ssl/private/maildev.key
--https-cert /etc/ssl/certs/maildev.crt
```

## Open maildev with https
```
https:\\192.168.1.103:1080
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
https:\\192.168.1.103:1080
https:\\192.168.1.103:1080
https://192.168.1.103:1080

```
As it's a self signed certificate, you need to accept it in your browser.
11 changes: 10 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ module.exports = function (config) {
.version(version)
.option('-s, --smtp <port>', 'SMTP port to catch emails [1025]', '1025')
.option('-w, --web <port>', 'Port to run the Web GUI [1080]', '1080')
.option('--https', 'Switch from http to https protocol')
.option('--https-key <file>', 'The file path to the ssl private key')
.option('--https-cert <file>', 'The file path to the ssl cert file')
.option('--ip <ip address>', 'IP Address to bind SMTP service to', '0.0.0.0')
.option('--outgoing-host <host>', 'SMTP host for outgoing emails')
.option('--outgoing-port <port>', 'SMTP port for outgoing emails')
Expand Down Expand Up @@ -76,9 +79,15 @@ module.exports = function (config) {
}

if (!config.disableWeb) {
const secure = {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this grouping of the options together. Makes me think that web.start()'s arguments should be refactored soon.

https: config.https,
cert: config.httpsCert,
key: config.httpsKey
}

// Default to run on same IP as smtp
const webIp = config.webIp ? config.webIp : config.ip
web.start(config.web, webIp, mailserver, config.webUser, config.webPass, config.basePathname)
web.start(config.web, webIp, mailserver, config.webUser, config.webPass, config.basePathname, secure)

if (config.open) {
const open = require('opn')
Expand Down
22 changes: 20 additions & 2 deletions lib/web.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
const express = require('express')
const cors = require('cors')
const http = require('http')
const https = require('https')
const fs = require('fs')
const socketio = require('socket.io')
const routes = require('./routes')
const auth = require('./auth')
Expand Down Expand Up @@ -73,9 +75,25 @@ web.server = null
* Start the web server
*/

web.start = function (port, host, mailserver, user, password, basePathname) {
web.start = function (port, host, mailserver, user, password, basePathname, secure) {
const app = express()
web.server = http.createServer(app)
if (secure.https) {
if (fs.existsSync(secure.key) === false) {
logger.error('Unable to find https secure key. Please specify key file via -https-key argument')
return
}
if (fs.existsSync(secure.cert) === false) {
logger.error('Unable to find https secure cert. Please specify cert file via -https-cert argument')
return
}
const options = {
key: fs.readFileSync(secure.key),
cert: fs.readFileSync(secure.cert)
}
web.server = https.createServer(options, app)
} else {
web.server = http.createServer(app)
}

if (user && password) {
app.use(auth(user, password))
Expand Down