Camera Motion -> Simple LAN SMTP Service -> Webhook


#1

I’ve got some Reolink Argus 2 cameras around the house. I wanted to set them up as motion detectors to turn on lights at night, but the only motion detection action these cameras can fire is email. I searched the world over for some sort of simple SMTP server that would trigger a webCoRE webhook. Couldn’t find any. Made my own. Now I will share them with the world.


For Windows users, here’s a Batch + PowerShell polyglot script. Save it with a .bat extension. Salt to taste:

<# : Batch portion
@echo off & setlocal

rem // When you've thoroughly tested the script and are convinced it is ready,
rem // change "minimized" to "hidden" in the following command to have this
rem // script run invisibly.

powershell -window minimized "iex (${%~f0} | out-string)"
goto :EOF

rem // end Batch / begin PowerShell polyglot #>

$webhook = 'https://graph-na04-useast2.api.smartthings.com/api/token/....'

# Camera determined by the username half of the From: address.  Example: driveway@home
$cameras = "driveway", "frontporch", "backyard", "doorbell"

$tcpClient = new-object Net.Sockets.TcpClient
$port = 25

$endpoint = new-object Net.IPEndPoint([Net.IPAddress]::Any, $port)
$listener = new-object Net.Sockets.TcpListener $endpoint
$listener.start()

function in($stream) {
	$request = new-object byte[] 32768
	$size = $stream.read($request, 0, $request.length)
	$msg = ([text.encoding]::UTF8.GetString($request, 0, $size)).Trim()
	write-host -f yellow "<- $msg"
	return $msg
}

function out($stream, $response) {
	write-host -f cyan "-> $response"
	$out = [text.encoding]::UTF8.GetBytes($response + "`r`n")
	$stream.write($out, 0, $out.length)
}

function btoa($txt) {
	return [convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($txt))
}

function trigger($cam) {
	$json = @{ "cam" = $cam } | ConvertTo-Json
	Invoke-WebRequest $webhook -Method post -ContentType 'application/json' -Body $json
}

try {
	while ($true) {
		while (!$listener.Pending()) {
			start-sleep -milliseconds 10
		}
		$client = $listener.AcceptTcpClient()
		$stream = $client.GetStream()
		if ($stream.CanWrite) {
			out $stream "220 ${env:computername} ESMTP"
			do {
				$rcv = in($stream)
				switch -regex ($rcv) {
					"(EH|HE)LO " {
						out $stream "250-${env:computername} Nice to meet you."
						out $stream "250 AUTH LOGIN PLAIN"
					}
					"^AUTH LOGIN$" {
						out $stream ("334 {0}" -f (btoa "Username:"))
						$rcv = in($stream)
						out $stream ("334 {0}" -f (btoa "Password:"))
						$rcv = in($stream)
						out $stream "235 Authentication successful"
					}
					"^MAIL FROM:\W*(?<cam>[^@]+)" {
						write-host -f green $matches.cam
						if ($cameras -contains $matches.cam) {
							trigger $matches.cam
						}
						out $stream "250 Accepted"
					}
					"^RCPT TO:" {
						out $stream "250 Accepted"
					}
					"^DATA$" {
						out $stream "354 End data with <CR><LF>.<CR><LF>"
					}
					"\.$" {
						out $stream "250 OK: message queued"
					}
					"^QUIT$" {
						out $stream "221 Bye"
					}
				}
			} while ($stream.CanRead -and $rcv -ne "QUIT" -and $rcv -ne "")
			$stream.close()
			$stream.dispose()
			$client.close()
		}
	}
}
finally {
	$listener.stop()
}

Once you’ve thoroughly tested it, cut or copy, navigate to shell:startup, then paste.


For Linux / Raspberry Pi, here’s a NodeJS alternative. Be sure to set the values not only for webhook, but also for your certificates for SSL / TLS (or comment out the secure, key, and cert lines); as well as fixing the server.listen line at the bottom.

I’ll assume you already have nodejs installed. Install dependencies if needed:

npm install fs
npm install smtp-server
npm install node-fetch

Now here’s the script:

#!/usr/bin/nodejs

const webhook = "https://graph-na04-useast2.api.smartthings.com/api/token/....";

// Camera determined by the username half of the From: address.  Example: driveway@home
const cameras = ["driveway", "frontporch", "backyard", "doorbell"]

const fs = require('fs');
const SMTPServer = require("smtp-server").SMTPServer;
const fetch = require('node-fetch');

const server = new SMTPServer({
	secure: true,
	key: fs.readFileSync("/etc/letsencrypt/live/YOUR.HOST.DOMAIN/privkey.pem"),
	cert: fs.readFileSync("/etc/letsencrypt/live/YOUR.HOST.DOMAIN/cert.pem"),

	authOptional: true,
	disabledCommands: ['STARTTLS'],
	allowInsecureAuth: true,
	disableReverseLookup: true,
//	logger: true,

	onAuth(auth, session, callback) { callback(null, { user: auth.username }); },

	onMailFrom(address, session, callback) {
//		console.log(address.address);
		var cam = address.address.split('@')[0];

		if (cameras.includes(cam)) {
			fetch(webhook, {
				method: 'post',
				body:    JSON.stringify({"cam": cam}),
				headers: { 'Content-Type': 'application/json' },
			})
			.catch(err => console.error(err));
		}
		return callback(); // accept the connection
	}
});
server.on("error", err => {
	console.log("Error %s", err.message);
});
server.listen(465, '10.0.1.3'); // bind to non-localhost IP to avoid conflicts with log mailer

Once you’ve thoroughly tested the script, add it to /etc/networking/if-up.d to have it start with your network (or wherever your distro’s if-up scripts live). If your distro does use /etc/networking/if-up.d, you can likely confirm successful installation thusly:

sudo run-parts --test /etc/network/if-up.d

Unfortunately, entware doesn’t seem to have the dependencies needed for this script. No running it from your router or NAS, I’m afraid.


Anyway, with both scripts, authentication is optional, and both scripts accept any username and password thrown at them. Both scripts will patiently wait for an email. As soon as the client announces the From: address, the scripts will scrape the username portion and, if it exists in your cameras array, pass it as the “cam” parameter to your webhook. So for an email sent with a From: address of driveway@home for example, the JSON payload posted to the webhook looks simply like this:

{
    "cam": "driveway"
}

… or similar. So in your webCoRE dashboard, you can set a variable to an Argument named cam. And if appropriate, turn on lights or whatever it is you kids are into these days. Example:

Took me a couple days to figure out that the Reolink SMTP client will only connect on ports 25 unencrypted, 465 SSL, 587 TLS, and custom ports also require TLS. But Reolink’s TLS implementation doesn’t play well with that of NodeJS for some reason. As a result, custom ports such as 2525 will refuse to connect regardless of whether the encryption check box is checked. Ultimately I had to use port 465 for myself. YMMV.

But the end result is worthwhile. I started out using mailgun, but the latency between motion and reaction was so slow as to be useless. I needed a LAN solution. These self-hosted scripts work much more quickly.

What am I forgetting?