diff --git a/testing/aaudit/APKBUILD b/testing/aaudit/APKBUILD
index 0dc7179e095e0c33617d81fa8866d58a075d7217..62edc714dbb966d5fc019928f14770e616ea0aa4 100644
--- a/testing/aaudit/APKBUILD
+++ b/testing/aaudit/APKBUILD
@@ -1,13 +1,13 @@
 # Contributor: Timo Teräs <timo.teras@iki.fi>
 # Maintainer: Timo Teräs <timo.teras@iki.fi>
 pkgname=aaudit
-pkgver=0.5
+pkgver=0.6
 pkgrel=0
 pkgdesc="Alpine Auditor"
 url="http://alpinelinux.org"
 arch="noarch"
 license="GPL"
-depends="lua5.2 lua5.2-posix lua5.2-cjson lua5.2-pc"
+depends="lua5.2 lua5.2-posix lua5.2-cjson lua5.2-pc lua5.2-socket"
 makedepends=""
 install=""
 subpackages="$pkgname-server"
@@ -17,6 +17,7 @@ source="aaudit-common.lua
 	aaudit
 	aaudit-shell
 	aaudit-update-keys
+	aaudit-repo
 	aaudit.json
 	aaudit-server.json
 	"
@@ -44,28 +45,31 @@ server() {
 		"$subpkgdir"/usr/share/lua/$_luaver/aaudit
 	cp aaudit-server.json "$subpkgdir"/etc/aaudit
 	cp aaudit-server.lua  "$subpkgdir"/usr/share/lua/$_luaver/aaudit/server.lua
-	cp aaudit-update-keys "$subpkgdir"/usr/bin
+	cp aaudit-update-keys aaudit-repo "$subpkgdir"/usr/bin
 	cp aaudit-shell       "$subpkgdir"/usr/libexec/aaudit
 }
 
 md5sums="b81b0707b297a69dacbc1606324de029  aaudit-common.lua
-2dfb114024f0f7aa46400a6c81d1d9cc  aaudit-server.lua
-9b48cc51f9f16989af4290107b2b866d  aaudit
+aaca139a4d476cadd1e7f8516caafec4  aaudit-server.lua
+028ecf2a733387560b15563de1777c68  aaudit
 f5de73f12b6df1a751c89f19c92871be  aaudit-shell
 feef077f56f40002ca11846512d347af  aaudit-update-keys
-e14ded329626ca1d6dd48e5bef0bc7e2  aaudit.json
-f2c15c547701ec71aa4d0c1d0f7b70b1  aaudit-server.json"
+9c88f34f561a2d0955b07de467b18a70  aaudit-repo
+8a80554c91d9fca8acb82f023de02f11  aaudit.json
+f0e97a7a07a472278298b5999a397f58  aaudit-server.json"
 sha256sums="ee1998e730356c2de0ff9d5e27d9e0277e3c1f051777146b7c5b820437edfd7f  aaudit-common.lua
-55f70bb0eb60b33580a6f68f57f0a8b52fef4f2bf4c4851af7a9b96c60f4841a  aaudit-server.lua
-160a4e2893dfb264a7844d911d7c2781dda4624f2357f3e7950338f8d2ca9c2a  aaudit
+fb4ae6432f0682fccd6926e0520037cf40bc41e8781c3dfe6e70101926c5588d  aaudit-server.lua
+91b5247e856b6531796a0ac61c3c82a37880fbabc1afc9bf4793667f03fb3ea0  aaudit
 659c755cfca95a76da78f4d28d0ab9a32d55bea0077be7420ceaf9d45c518354  aaudit-shell
 660dcf86f02a9d0e3ff47cb359e0291a0921d03215e368552a2878d2d691a9cc  aaudit-update-keys
-f61efebc04756c8bfb7cb955b7af5db6a3c5dabdd005f690db812c7e77567cf5  aaudit.json
-d477ca7714ff43818f8deef3f60d69d118ea899c57b6109cdf4c6c22009cc07c  aaudit-server.json"
+83868f17e1162e2b621eb2115a36f989c300aeda7cadf82ec1c991ee19d25664  aaudit-repo
+ca3d163bab055381827226140568f3bef7eaac187cebd76878e0b63e9e442356  aaudit.json
+ab276c9caeaa238c8999c79cd3b016f4ba149e6793475c5cde088597cedd849c  aaudit-server.json"
 sha512sums="aaa378fd710d17cb3663954648e97dd5128406cc6f37e9834075046aed1912dcc9e448b6c96502350b8d3496e00b7803cae671a4be2c12c584a84dc0b6e843e9  aaudit-common.lua
-72fb60c37a54b34feff48d1658ecc341937c9fb9148dafd71e19347c6e1cd2daf4f59dfb443490b14f82f543847a56b04c877eb545dc9ee6b4af310a94e1008d  aaudit-server.lua
-1c9a78d1d2557353982ae5edbad7fefb2983ba06ff139352d2106dbba00f675cc50050ad5dd7a62153b8c8c8a6ec953872cb5c0289fa999656ade8f8004a3f5e  aaudit
+a78e3600fbac793040f3ece9171a47e8e624d26860b1f68bfca8812a07592c7c8f70e2806184a2359f81f5fb43d67520eb2de3f3f2145804dd1a8ee1266e1a45  aaudit-server.lua
+e12b1623506382e04307f1c7fc361b544b4fbe992d41fcbaa7efd4c8568060fa7fad17e4db7a4ae96ebadc2b95f3c545809b948460a5446608bac6a35d3c35f3  aaudit
 18499771d7d425f9305209c562eb8e62ef41910e88b08219baf010cdb472d49087080feb67384c4826c53bddcc0ce92c0c23c78df22dc40c64f1b17bf0ad05ec  aaudit-shell
 aec728a9a1e4c92baeb94a9d95e1785ea166652a157571fe2e848e71c1246635ecb99512e92435e1314c620b1fa8e4f37400350bed78bd375db4a63828c500f0  aaudit-update-keys
-e769f0f77fe54ba1ab35efc80cc6426e34a2ee1d053ac9e7cc5aa316cfcef0c9658d2f0e2c47f7ae282bb9cc07107065fcc13034b2f9125c182378b7c73b7d99  aaudit.json
-69f8da6b6057dfb5ab0f36f8b27db3a275cd24e4d45a0e95cf4a79d955c0ebfc88861a0a50d5b6da542587cbe8feca1842c966026f0bf445edea316202d49ee5  aaudit-server.json"
+7507dea2b8ec4054e507aa3b42818863b9737402f3a9f967e16a63dd3ea12385463c2bce178a819a5f1fef76e74887a136de665e0b54172f1c1ce5f61b73403f  aaudit-repo
+ca4b6defb8adcc010050bc8b1bb8f8092c4928b8a0fba32146abcfb256e4d91672f88ca2cdf6210e754e5b8ac5e23fb023806ccd749ac8b701f79a691f03c87a  aaudit.json
+52da35598b8638a34d5a6352b2ccfd046dc529e0e5e6b541d3111016cbe6b091ff3fb4175e98b4f39e226ca1e6c973b9aa9a7a74fcb49b41862bbe64979d9186  aaudit-server.json"
diff --git a/testing/aaudit/aaudit b/testing/aaudit/aaudit
index f17c6cc6a8eccbd09b9f58839f714c37372b4922..44652a1deb689c9e6570fb10411e1e3b7f9b213f 100755
--- a/testing/aaudit/aaudit
+++ b/testing/aaudit/aaudit
@@ -9,17 +9,15 @@ local function usage()
 	print([[
 Usage: aaudit [create|commit] [OPTIONS...]
 
-Valid options for create:
+Options for create:
 	-s SERV	Use server SERV
 	-d DESC	Description for repository (default: hostname)
 	-t ADDR	Specify ADDR as target device (default: local source IP)
-	-m MSG	Specify message for the initial commit
 	-g GRP	Add in group GRP (can be specified multiple times)
 
-Valid options for commit:
+Options for create and commit:
 	-m MSG	Specify message for the commit
-	-r RT	Related to ticket RT
-	-c RT	Closes ticket RT
+	-L	Local change (use local 'contact' as change author)
 ]])
 	os.exit(1)
 end
@@ -28,7 +26,7 @@ local verbose = false
 local conf = aac.readconfig() or {}
 local req = {}
 
-for ret, optval in posix.getopt(arg, 'vs:d:t:m:g:r:c:') do
+for ret, optval in posix.getopt(arg, 'vs:d:t:m:Lg:') do
 	if ret == 'v' then
 		verbose = true
 	elseif ret == 's' then
@@ -39,14 +37,11 @@ for ret, optval in posix.getopt(arg, 'vs:d:t:m:g:r:c:') do
 		conf.target_address = optval
 	elseif ret == 'm' then
 		req.message = optval
+	elseif ret == 'L' then
+		req.local_change = true
 	elseif ret == 'g' then
 		req.groups = req.groups or {}
 		table.insert(req.groups, optval)
-	elseif ret == 'r' then
-		req.ticket = optval
-	elseif ret == 'c' then
-		req.ticket = optval
-		req.ticket_action = "close"
 	else
 		usage()
 	end
diff --git a/testing/aaudit/aaudit-repo b/testing/aaudit/aaudit-repo
new file mode 100755
index 0000000000000000000000000000000000000000..dbacd976bf1b7386c91f849be650830ecd5419cf
--- /dev/null
+++ b/testing/aaudit/aaudit-repo
@@ -0,0 +1,47 @@
+#!/usr/bin/lua5.2
+
+local posix = require 'posix'
+local aac = require 'aaudit.common'
+local aas = require 'aaudit.server'
+
+local pullafter = aas.serverconfig["pull-after"] or 24*60*60
+local warnafter = aas.serverconfig["warn-after"] or 4*24*60*60
+
+local function dorepo(repodir)
+	-- Check if it's time to update
+	local repoconf = aas.loadrepoconfig(repodir)
+	local stampfile = ("%s/lastcheck"):format(repodir)
+	local mtime = posix.stat(stampfile, "mtime") or 0
+
+	if os.time() > mtime + pullafter then
+		-- Pull for changes
+		local req = {
+			command = "commit",
+			target_address = repoconf.address,
+			message = "Unexpected configuration change",
+			local_change = true,
+		}
+		local ret, msg = aas.handle(req)
+		print(("Updating repository %s -> %s: %s"):format(repodir, repoconf.address, msg))
+		mtime = posix.stat(stampfile, "mtime") or 0
+	end
+
+	return mtime, repoconf.address
+end
+
+local home = os.getenv("HOME")
+local outdated = {"List of unreachable monitored hosts:"}
+for _, repodir in ipairs(posix.glob(("%s/*.git"):format(home))) do
+	local mtime, address = dorepo(repodir)
+	if os.time() > mtime + warnafter then
+		table.insert(outdated, address)
+	end
+end
+
+if #outdated > 1 and aas.serverconfig["notify-unreachables"] then
+	aas.sendemail {
+		to = aas.serverconfig["notify-unreachables"],
+		subject = "aaudit report of unreachable hosts",
+		message = table.concat(outdated, "\n"),
+	}
+end
diff --git a/testing/aaudit/aaudit-server.json b/testing/aaudit/aaudit-server.json
index 269e6171667504763c6a2b798db8401210e6c90c..d00c0be8121a7115652d66354b06617fcd10aa10 100644
--- a/testing/aaudit/aaudit-server.json
+++ b/testing/aaudit/aaudit-server.json
@@ -1,5 +1,6 @@
 {
   "smtp_server": "localhost",
+  "rtqueue": "rtqueue",
   "identities": {
     "_default": "Alpine Auditor <auditor@alpine.local>"
   },
diff --git a/testing/aaudit/aaudit-server.lua b/testing/aaudit/aaudit-server.lua
index e6ed3a65e3c76d31bce2970784838412c204fc68..89f2fc0219ee506e14b34be493c8d97b84059438 100644
--- a/testing/aaudit/aaudit-server.lua
+++ b/testing/aaudit/aaudit-server.lua
@@ -4,9 +4,12 @@ local posix = require 'posix'
 local json = require 'cjson'
 local zlib = require 'zlib'
 local aac = require 'aaudit.common'
+local smtp = require 'socket.smtp'
 
 local HOME = os.getenv("HOME")
 
+M.serverconfig = aac.readconfig(("%s/aaudit-server.json"):format(HOME)) or {}
+
 local function merge_bool(a, b) return a or b end
 local function merge_array(a, b) if b then for i=1,#b do a[#a+1] = b[i] end end return a end
 
@@ -70,6 +73,85 @@ local function read_header_block(block)
 	return header
 end
 
+local function rfc822_address(id)
+	local identities = M.serverconfig.identities
+	if id == nil then id = "_default" end
+	if identities and identities[id] then id = identities[id] end
+	local name, email = id:match("^(.-) *(<.*>)$")
+	if not email then return ("<%s>"):format(id) end
+	return ("%s %s"):format(name, email)
+end
+
+local function rfc822_email(rfc822)
+	return rfc822:match("(<.*>)$")
+
+end
+
+function M.sendemail(mail)
+	local to = {}
+	local m = {
+		headers = {
+			["Content-Type"] = 'text/plain; charset=utf8',
+			["X-RT-Command"] = mail.rtheader,
+			from = rfc822_address(mail.from),
+			subject = mail.subject,
+		},
+		body = mail.message,
+	}
+	local rcpt = {}
+	for _, addr in ipairs(mail.to) do
+		local rfc822 = rfc822_address(addr)
+		table.insert(to, rfc822)
+		table.insert(rcpt, rfc822_email(rfc822))
+	end
+	m.headers.to = table.concat(to, ", ")
+	return smtp.send{
+		from = rfc822_email(m.headers.from),
+		rcpt = rcpt,
+		source = smtp.message(m)
+	}
+end
+
+local rt_keywords = {
+	fix = true,
+	fixes = true,
+	close = true,
+	closes = true,
+	ref = false,
+	refs = false,
+	rt = false,
+}
+
+local function sendcommitdiff(body, req, R, G)
+	if not body then return end
+	if not G.notify_emails then return end
+
+	local mail = {
+		from = req.committer,
+		to = G.notify_emails,
+		subject = ("config change - %s (%s)"):format(R.description, R.address),
+		message = table.concat(body, '\n')
+	}
+
+	-- Set Request Tracker headers if relevant
+	local rtqueue = M.serverconfig.rtqueue
+	if rtqueue then
+		for k,no in req.message:gmatch("(%a+) #(%d+)") do
+			local action = rt_keywords[k]
+			if action ~= nil then
+				mail.subject = ("[%s #%s] %s"):format(rtqueue, no, mail.subject)
+				if action == true then
+					mail.rtheader = "Status: resolved"
+				end
+				break
+			end
+		end
+	end
+
+	-- Send email
+	return M.sendemail(mail)
+end
+
 local function import_tar(TAR, GIT, req, G)
 	local branch_ref = "refs/heads/import"
 	local from_ref = "refs/heads/master"
@@ -107,6 +189,17 @@ local function import_tar(TAR, GIT, req, G)
 			end
 		end
 
+		if header.name == "etc/aaudit/aaudit.json" then
+			local success, res = pcall(json.decode, file_data)
+			if success and res.contact then
+				local contact = res.contact
+				G.notify_emails = merge_array(G.notify_emails, {contact})
+				if req.local_change then
+					req.author = rfc822_address(res.contact)
+				end
+			end
+		end
+
 		if header.typeflag:match("^[0-46]$") and
 		   not match_file(header.name, G.no_track) then
 			GIT:write('blob\n', 'mark :', nextmark, '\n')
@@ -140,9 +233,9 @@ data <<END_OF_COMMIT_MESSAGE
 END_OF_COMMIT_MESSAGE
 
 ]]):format(branch_ref,
-	req.author.rfc822, author_time,
-	req.author.rfc822, os.time(),
-	req.message or "Changes"))
+	req.author, author_time,
+	req.committer, os.time(),
+	req.message))
 
 	if not req.initial then GIT:write(("from %s^0\n"):format(from_ref)) end
 	GIT:write("deleteall\n")
@@ -191,70 +284,31 @@ local function generate_diff(repodir, commit, G)
 	return has_changes, text
 end
 
-local function resolve_email(identities, id)
-	if identities and identities[id] then id = identities[id] end
-	local name, email = id:match("^(.-) *<(.*)>$")
-	if email then return {name=name, email=email, rfc822=("%s <%s>"):format(name, email) } end
-	return {name="", email=name, rfc822=("<%s>"):format(name)}
-end
-
-local function send_email(body, req, S, R, G)
-	if not body then return end
-	if not G.notify_emails then return end
-
-	local to_rfc822 = {}
-	local to_email = {}
-	for _,r in ipairs(G.notify_emails) do
-		local id = resolve_email(S.identities, r)
-		if not to_email[id.email] then
-			to_email[id.email] = true
-			table.insert(to_rfc822, id.rfc822)
-			table.insert(to_email, id.email)
-		end
-	end
-	to_rfc822 = table.concat(to_rfc822, ", ")
-	to_email  = table.concat(to_email, " ")
-
-	-- Add busybox sendmail smtp server option
-	local options=""
-	if S.smtp_server then options = ('-S "%s"'):format(S.smtp_server) end
-
-	local EMAIL = io.popen(('sendmail -f "%s" %s %s'):format(req.author.email, options, to_email), "w")
-	EMAIL:write(([[
-From: %s
-To: %s
-Subject: apkovl changed - %s (%s)
-Date: %s
-
-]]):format(req.author.rfc822, to_rfc822, R.description, R.address, os.date("%a, %d %b %Y %H:%M:%S")))
-
-	for _, l in ipairs(body) do EMAIL:write(l,'\n') end
-	EMAIL:close()
-
-	return to_email
+function M.loadrepoconfig(repohome)
+	return aac.readconfig(("%s/aaudit-repo.json"):format(repohome))
 end
 
 local function load_repo_configs(repohome)
-	local S = aac.readconfig(("%s/aaudit-server.json"):format(HOME))
-	local R = aac.readconfig(("%s/aaudit-repo.json"):format(repohome))
+	local R = M.loadrepoconfig(repohome)
 	-- merge global and per-repository group configs
-	local G = (S.groups or {}).all or {}
+	local G = (M.serverconfig.groups or {}).all or {}
 	for _, name in pairs(R.groups or {}) do
-		local g = S.groups[name] or {}
+		local g = M.serverconfig.groups[name] or {}
 		G.notify_emails = merge_array(G.notify_emails, g.notify_emails)
 		G.track_filemode = merge_bool(G.track_filemode, g.track_filemode)
 		G.no_track = merge_array(G.no_track, g.no_track)
 		G.no_notify = merge_array(G.no_notify, g.no_notify)
 		G.no_diff = merge_array(G.no_diff, g.no_diff)
 	end
-	return S, R, G
+	return R, G
 end
 
 function M.repo_update(req,clientstream)
 	local repodir = req.repositorydir
-	local S, R, G = load_repo_configs(repodir)
+	local R, G = load_repo_configs(repodir)
 
-	req.author = resolve_email(S.identities, req.identity)
+	req.committer = rfc822_address(req.identity)
+	req.author = req.committer
 
 	local TAR
 	if req.apkovl_follows then
@@ -269,16 +323,26 @@ function M.repo_update(req,clientstream)
 	TAR:close()
 	if not rc then return rc, err end
 
+	local stampfile = ("%s/lastcheck"):format(repodir)
+	if posix.utime(stampfile) ~= 0 then
+		posix.close(posix.open(stampfile, posix.O_CREAT, "0644"))
+	end
+
 	local has_changes, email_body = generate_diff(repodir, "import", G)
 	if has_changes then
+		if not req.initial then
+			local res, err = sendcommitdiff(email_body, req, R, G)
+			if not res then
+				os.execute(("git --git-dir='%s' branch --quiet -D import;"..
+					    "git --git-dir='%s' gc --quiet --prune=now")
+					:format(repodir, repodir))
+				return false, err
+			end
+		end
 		os.execute(("git --git-dir='%s' branch --quiet --force master import;"..
 			    "git --git-dir='%s' branch --quiet -D import")
 			:format(repodir, repodir))
-		local to = nil
-		if not req.initial then
-			to = send_email(email_body, req, S, R, G)
-		end
-		return true, "Committed", {notified=to}
+		return true, "Committed"
 	end
 
 	os.execute(("git --git-dir='%s' branch --quiet -D import;"..
@@ -322,6 +386,7 @@ function M.handle(req,clientstream)
 		req.command = "commit"
 	end
 	if req.command == "commit" then
+		req.message = req.message or "Configuration change"
 		if not posix.access(req.repositorydir, "rwx") then
 			return false, "No such repository"
 		end
diff --git a/testing/aaudit/aaudit.json b/testing/aaudit/aaudit.json
index 958d60fbc620f0a267bbbf8ecc7b7fe3347b29a9..0967ef424bce6791893e9a57bb952f80fd536e93 100644
--- a/testing/aaudit/aaudit.json
+++ b/testing/aaudit/aaudit.json
@@ -1,5 +1 @@
-{
-  "user": "aaudit",
-  "server": "aaudit.alpine.local",
-  "rtqueue": "alpine.org"
-}
+{}