summaryrefslogtreecommitdiffstats
path: root/fetchmail-model.lua
blob: 0ab4a3dc37f3cccedbc26afe097733d9b22ede20 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
module(..., package.seeall)

-- Load libraries
require("modelfunctions")
require("posix")
fs = require("acf.fs")
format = require("acf.format")
validator = require("acf.validator")

-- Set variables
local packagename = "fetchmail"
local processname = "fetchmail"
local configfile = "/etc/fetchmailrc"
local confdfile = "/etc/conf.d/fetchmail"
local config

local methods = {"pop3","imap","pop3domain", "etrn", }

-- ################################################################################
-- LOCAL FUNCTIONS

local function findkeywordsinentry(entry)
	local reverseentry = {}
	-- we can't just do a simple reverse table in case keywords are used as parameters (ie. password = password)
	--for i,word in ipairs(entry) do reverseentry[word] = i end
	-- so, have to parse word by word
	-- the following is a list of the keywords we know about in this ACF
	-- dns, fetchall, fetchdomains, is, local, localdomains, pass, password, proto, protocol, rewrite, smtphost, ssl, user, username, envelope
	-- array of keywords that take at least one parameter (don't know how to handle multiple parameters)
	local keywords = { "fetchdomains", "is", "local", "localdomains", "pass", "password", "proto", "protocol", "smtphost", "user", "username" }
	local reversekeywords = {}
	for i,word in ipairs(keywords) do reversekeywords[word] = i end

	local i=0
	while i<#entry do
		i = i+1
		reverseentry[entry[i]] = i
		if reversekeywords[entry[i]] then
			i = i+1
		end
	end

	return reverseentry
end

local function parseconfigfile(file)
	file = file or ""
	local retval = {}
	local linenum=0
	for line in string.gmatch(file, "([^\n]*)\n?") do
		linenum=linenum+1
		if not string.match(line, "^%s*$") and not string.match(line, "^%s*#") then
			table.insert(retval, {linenum=linenum})
			-- Iterate through each word, being careful about quoted strings and comments
			local offset = 1
			while string.find(line, "%S+", offset) do
				local word = string.match(line, "%S+", offset)
				local endword = select(2, string.find(line, "%S+", offset))
				if string.find(word, "^#") then
					break
				elseif string.find(word, "^\"") then
					endword = select(2, string.find(line, "\"[^\"]*\"", offset))
					word = string.sub(line, string.find(line, "\"", offset)+1, endword-1)
				end
				table.insert(retval[#retval], word)
				offset = endword + 1
			end
		end
	end
	return retval
end

local function findentryline(entryname, method, remotemailbox, localdomain)
	if entryname and entryname ~= "" then
		config = config or parseconfigfile(fs.read_file(configfile) or "")
		for i,entry in ipairs(config or {}) do
			if (entry[1] == "server" or entry[1] == "poll" or entry[1] == "skip") and entry[2] == entryname then
				local reverseentry = findkeywordsinentry(entry)
				-- For pop3domain, check the localdomain
				if method == "pop3domain" and (reverseentry["local"] or reverseentry["localdomains"]) then
					if entry[(reverseentry["local"] or reverseentry["localdomains"])+1] == localdomain then
						return entry
					end
				elseif reverseentry["proto"] or reverseentry["protocol"] then
					local protocol = entry[(reverseentry["proto"] or reverseentry["protocol"])+1]
					-- For etrn, no further check
					if method == "etrn" and protocol == "etrn" then
						return entry
					-- For pop3 and imap, check the username
					elseif protocol == method and (method == "pop3" or method == "imap") then
						if reverseentry["username"] and entry[reverseentry["username"]+1] == remotemailbox then
							return entry
						end
					end
				end
			end
		end
	end
	return nil
end

local function writeentryline(entrystruct, entryline)
	if not entrystruct and not entryline then
		return
	end

	-- If there is a structure, create the entryline array
	if entrystruct then
		-- We'll use a reverseentry array to tell where entries are in entryline
		local reverseentry = {}
		if entryline then
			reverseentry = findkeywordsinentry(entryline)
			-- to handle entries that have multiple names
			function equateentries(option1,option2)
				if reverseentry[option1] then
					reverseentry[option2] = reverseentry[option1]
				else
					reverseentry[option1] = reverseentry[option2]
				end
			end
			equateentries("proto", "protocol")
			equateentries("local", "localdomains")
			equateentries("auth", "authenticate")
			equateentries("user", "username")
			equateentries("pass", "password")
		else
			entryline = {}
		end

		-- From http://fetchmail.berlios.de/fetchmail-man.html:
		-- "All user options must begin with a user description (user or username option)
		-- and follow all server descriptions and options."
		-- So, what we'll do is build two new server/user option arrays.
		-- Then, we'll add in the options we don't understand and combine the arrays.
		-- So, our options will be in order and unknown ones will be at the end of each section.
		local serveroptions = {}
		local useroptions = {}
		-- Here are some helper functions to set option values and delete from entryline
		function setserveroption(option, value)
			serveroptions[#serveroptions+1] = option
			serveroptions[#serveroptions+1] = value
			deleteoptionandvalue(option)
		end
		function setuseroption(option, value)
			useroptions[#useroptions+1] = option
			useroptions[#useroptions+1] = value
			deleteoptionandvalue(option)
		end
		function deleteoption(option)
			if reverseentry[option] then
				entryline[reverseentry[option]] = nil
			end
		end
		function deleteoptionandvalue(option)
			if reverseentry[option] then
				entryline[reverseentry[option]] = nil
				entryline[reverseentry[option]+1] = nil
			end
		end
		function deletenooption(option)
			if reverseentry[option] then
				entryline[reverseentry[option]] = nil
				local test = entryline[reverseentry[option]-1]
				if test and test == "no" then
					entryline[reverseentry[option]-1] = nil
				end
			end
		end

		-- Now we can start to set stuff
		-- First, the first two entries
		if entrystruct.value.enabled.value then
			serveroptions[1] = "poll"
		else
			serveroptions[1] = "skip"
		end
		serveroptions[2] = entrystruct.value.remotehost.value
		entryline[1] = nil
		entryline[2] = nil
		-- remove here and there
		if reverseentry.here then
			entryline[reverseentry.here] = nil
		end
		if reverseentry.there then
			entryline[reverseentry.there] = nil
		end

		-- Now we get to the interesting stuff
		if entrystruct.value.method.value == "etrn" then
			deleteoptionandvalue("username")
			deleteoptionandvalue("password")
			deleteoptionandvalue("is")
			deleteoptionandvalue("smtphost")
			deleteoption("ssl")
			deletenooption("rewrite")
			deleteoption("fetchall")
			deletenooption("dns")
		else	-- Method not etrn
			setuseroption("username", entrystruct.value.remotemailbox.value)
			setuseroption("password", '"'..entrystruct.value.remotepassword.value..'"')
			if entrystruct.value.method.value == "pop3domain" then
				deleteoptionandvalue("is")
			else
				setuseroption("is", entrystruct.value.localmailbox.value)
			end
			setuseroption("smtphost", entrystruct.value.localhost.value)
			if entrystruct.value.ssl.value and not reverseentry["ssl"] then
				useroptions[#useroptions+1] = "ssl"
			elseif not entrystruct.value.ssl.value and reverseentry["ssl"] then
				entryline[reverseentry["ssl"]] = nil
			end
			if not reverseentry["rewrite"] then
				useroptions[#useroptions+1] = "no"
				useroptions[#useroptions+1] = "rewrite"
			end
			if not reverseentry["fetchall"] then
				useroptions[#useroptions+1] = "fetchall"
			end
			if not reverseentry["dns"] then
				serveroptions[#serveroptions+1] = "no"
				serveroptions[#serveroptions+1] = "dns"
			end
		end
		if entrystruct.value.method.value == "pop3domain" then
			setserveroption("protocol", "pop3")
			setserveroption("localdomains", entrystruct.value.localdomain.value)
			setuseroption("to", "*")
			setuseroption("smtpaddress", entrystruct.value.localdomain.value)
			deleteoptionandvalue("fetchdomains")
		elseif entrystruct.value.method.value == "etrn" then
			setserveroption("protocol", entrystruct.value.method.value)
			deleteoptionandvalue("localdomains")
			deleteoptionandvalue("to")
			deleteoptionandvalue("smtpaddress")
			setuseroption("fetchdomains", entrystruct.value.localdomain.value)
		else	-- Method not pop3domain or etrn
			setserveroption("protocol", entrystruct.value.method.value)
			deleteoptionandvalue("localdomains")
			deleteoptionandvalue("to")
			deleteoptionandvalue("smtpaddress")
			deleteoptionandvalue("fetchdomains")
		end
		-- envelope is tough because it may have a no before or one or two values after
		-- first, delete any envelope option (preserving the count)
		local envelopecount
		if reverseentry["envelope"] then
			if entryline[reverseentry["envelope"]-1] == "no" then
				entryline[reverseentry["envelope"]-1] = nil
			else
				if validator.is_integer(entryline[reverseentry["envelope"]+1] or "") then
					-- Keep the number if not changing envelope option
					if entryline[reverseentry["envelope"]+2] == entrystruct.value.envelope.value then
						envelopecount = entryline[reverseentry["envelope"]+1]
					end
					entryline[reverseentry["envelope"]+2] = nil
				end
				entryline[reverseentry["envelope"]+1] = nil
			end
			entryline[reverseentry["envelope"]] = nil
		end
		if entrystruct.value.method.value == "pop3domain" then
			if entrystruct.value.envelope.value == "disabled" then
				
				serveroptions[#serveroptions+1] = "no"
				serveroptions[#serveroptions+1] = "envelope"
			else
				serveroptions[#serveroptions+1] = "envelope"
				serveroptions[#serveroptions+1] = envelopecount
				serveroptions[#serveroptions+1] = entrystruct.value.envelope.value
			end
		end

		-- Now, insert the remaining options
		for i=1,reverseentry["username"] or table.maxn(entryline) do
			serveroptions[#serveroptions+1] = entryline[i]
		end
		for i=reverseentry["username"] or table.maxn(entryline), table.maxn(entryline) do
			useroptions[#useroptions+1] = entryline[i]
		end
		local linenum = entryline.linenum
		entryline = serveroptions
		for i,val in ipairs(useroptions) do
			entryline[#entryline+1] = val
		end
		entryline.linenum = linenum
	end

	local file = fs.read_file(configfile) or ""
	local lines = {file}

	if entryline and entryline.linenum then
		-- Split the file to remove the line
		local startchar, endchar = string.match(file, string.rep("[^\n]*\n", entryline.linenum-1) .. "()[^\n]*\n()")
		if startchar and endchar then
			lines[1] = string.sub(file, 1, startchar-1)
			lines[2] = string.sub(file, endchar, -1)
		end
	end
	if entryline and entrystruct then
		table.insert(lines, 2, table.concat(entryline," ").."\n")
	end

	fs.write_file(configfile, string.gsub(table.concat(lines), "\n+$", ""))
	posix.chmod(configfile, "rw-------")
	posix.chown(configfile, posix.getpasswd("fetchmail", "uid") or 0)
	config = nil
end

local function validateentry(entry)
	local success = true

	function cannotbeblank(value)
		if value.value == "" then
			value.errtxt = "Invalid entry - cannot be blank"
			success = false
		end
	end
	function mustbeblank(value)
		if value.value ~= "" then
			value.errtxt = "Invalid entry - must be blank for this method"
			success = false
		end
	end

	success = modelfunctions.validateselect(entry.value.method) and success
	success = modelfunctions.validateselect(entry.value.envelope) and success
	if string.find(entry.value.remotehost.value, "[^%w%.%-]") then
		entry.value.remotehost.errtxt = "Invalid entry - may only contain alphanumeric, '.', or '-'"
		success = false
	end
	if string.find(entry.value.remotemailbox.value, "[^%w%.%-_@]") then
		entry.value.remotemailbox.errtxt = "Invalid entry"
		success = false
	end
	if string.find(entry.value.remotepassword.value, "%s") then
		entry.value.remotepassword.errtxt = "Invalid entry - cannot contain whitespace"
		success = false
	end
	if string.find(entry.value.localhost.value, "[^%w%.%-]") then
		entry.value.localhost.errtxt = "Invalid entry - may only contain alphanumeric, '.', or '-'"
		success = false
	end
	if string.find(entry.value.localmailbox.value, "[^%w%.%-_@]") then
		entry.value.localmailbox.errtxt = "Invalid entry"
		success = false
	end
	if string.find(entry.value.localdomain.value, "[^%w%.%-]") then
		entry.value.localdomain.errtxt = "Invalid entry - may only contain alphanumeric, '.', or '-'"
		success = false
	end
	cannotbeblank(entry.value.remotehost)
	if entry.value.method.value == "etrn" then
		mustbeblank(entry.value.remotemailbox)
		mustbeblank(entry.value.remotepassword)
		mustbeblank(entry.value.localhost)
		mustbeblank(entry.value.localmailbox)
		cannotbeblank(entry.value.localdomain)
	else
		cannotbeblank(entry.value.remotemailbox)
		cannotbeblank(entry.value.remotepassword)
		cannotbeblank(entry.value.localhost)
		if entry.value.method.value == "pop3domain" then
			mustbeblank(entry.value.localmailbox)
			cannotbeblank(entry.value.localdomain)
		else
			cannotbeblank(entry.value.localmailbox)
			mustbeblank(entry.value.localdomain)
		end
	end

	return success, entry
end

local function validateconfig(conf)
	local success = true
	if not validator.is_integer(conf.value.interval.value) then
		conf.value.interval.errtxt = "Invalid entry - must be an integer number"
		success = false
	end
	if string.find(conf.value.postmaster.value, "[^%w%.%-_@]") then
		conf.value.postmaster.errtxt = "Invalid entry"
		success = false
	end

	return success, conf
end

-- ################################################################################
-- PUBLIC FUNCTIONS

function get_startstop(self, clientdata)
        local actions = {"Run", "Test"}
	return cfe({ type="group", label="Management", value={}, option=actions })
end

function startstop_service(self, startstop, action)
	if action and (action:lower() == "run" or action:lower() == "test") then
		local cmd
		if action:lower() == "run" then
			cmd = 'su -s /bin/sh -c "/usr/bin/fetchmail -d0 -v --nosyslog -f '..configfile..' 2>&1" - fetchmail'
		elseif action:lower() == "test" then
			cmd = 'su -s /bin/sh -c "/usr/bin/fetchmail -d0 -v -k --nosyslog -f '..configfile..' 2>&1" - fetchmail'
		end
		local f = io.popen(cmd)
		startstop.descr = f:read("*a")
		f:close()
	else
		startstop.errtxt = "Invalid action"
	end
	return startstop
end

function getstatus()
	return modelfunctions.getstatus(processname, packagename, "Fetchmail Status")
end

function get_filedetails()
	-- FIXME - validation
	return modelfunctions.getfiledetails(configfile)
end

function update_filecontent(self, filedetails)
	-- FIXME - validation
	local retval = modelfunctions.setfiledetails(self, filedetails, {configfile})
	posix.chmod(configfile, "rw-------")
	posix.chown(configfile, posix.getpasswd("fetchmail", "uid") or 0)
	config = nil
	return retval
end

function getconfig()
	local interval = cfe({ value="60", label="Polling Interval", descr="Interval in seconds" })
	local postmaster = cfe({ label="Postmaster", descr="If defined, undeliverable mail is sent to this account, otherwise it is discarded" })
	local bounceerrors = cfe({ type="boolean", value=true, label="Bounce Errors", descr="Bounce errors back to the sender or send them to the postmaster" })

	config = config or parseconfigfile(fs.read_file(configfile) or "")
	for i,entry in ipairs(config or {}) do
		if entry[2] == "postmaster" and entry[1] == "set" then
			postmaster.value = entry[3] or ""
		elseif entry[3] == "bouncemail" and entry[2] == "no" and entry[1] == "set" then
			bounceerrors.value = false
		end
	end

	local confd = format.parse_ini_file(fs.read_file(confdfile) or "", "", "polling_period")
	if confd then
		interval.value = string.sub(confd, 2, -2)
	end

	return cfe({ type="group", value={interval=interval, postmaster=postmaster, bounceerrors=bounceerrors}, label="Fetchmail Global Config" })
end

function updateconfig(self, conf)
	local success, conf = validateconfig(conf)

	if success then
		local file = fs.read_file(configfile) or ""
		local foundpostmaster, foundbounceerrors
		local lines = {}
		for line in string.gmatch(file, "([^\n]*\n?)") do
			if not foundpostmaster and string.match(line, "^%s*set%s+postmaster%s") then
				foundpostmaster = true
				if conf.value.postmaster.value ~= "" then
					line = "set postmaster "..conf.value.postmaster.value.."\n"
				else
					line = nil
				end
			elseif not foundbounceerrors and string.match(line, "^%s*set%s+no%s+bouncemail%s") then
				foundbounceerrors = true
				if conf.value.bounceerrors.value then
					line = nil
				end
			end
			lines[#lines + 1] = line
		end
		if not foundpostmaster then
			table.insert(lines, 1, "set postmaster "..conf.value.postmaster.value.."\n")
		end
		if not foundbounceerrors and not conf.value.bounceerrors.value then
			table.insert(lines, 1, "set no bouncemail\n")
		end

		fs.write_file(configfile, table.concat(lines))
		posix.chmod(configfile, "rw-------")
		posix.chown(configfile, posix.getpasswd("fetchmail", "uid") or 0)
		config = nil

		fs.write_file(confdfile, format.update_ini_file(fs.read_file(confdfile) or "", "", "polling_period", '"'..conf.value.interval.value..'"'))
	else
		conf.errtxt = "Failed to set configuration"
	end

	return conf
end

function readentries()
	local entries = cfe({ type="structure", value={}, label="List of Fetchmail entries" })
	config = config or parseconfigfile(fs.read_file(configfile) or "")
	for i,entry in ipairs(config or {}) do
		if (entry[1] == "server" or entry[1] == "poll" or entry[1] == "skip") and entry[2] then
			local reverseentry = findkeywordsinentry(entry)
			local method = "error"
			local localdomain = ""
			if reverseentry["local"] or reverseentry["localdomains"] then
				method = "pop3domain"
				if entry[(reverseentry["local"] or reverseentry["localdomains"])+1] then
					localdomain = entry[(reverseentry["local"] or reverseentry["localdomains"])+1]
				end
			elseif reverseentry["proto"] or reverseentry["protocol"] then
				method = entry[(reverseentry["proto"] or reverseentry["protocol"])+1] or method
			end
			local enabled = true
			if entry[1] == "skip" then enabled=false end
			local username = ""
			if reverseentry["username"] and entry[reverseentry["username"]+1] then
				username = entry[reverseentry["username"]+1]
			end
			table.insert(entries.value, {remotehost=entry[2], method=method, enabled=enabled, remotemailbox=username, localdomain=localdomain})
		end
	end
	return entries
end

function readentry(entryname, meth, remotemailbx, localdom)
	local enabled = cfe({ type="boolean", value=true, label="Enable" })
	local method = cfe({ type="select", value="pop3", label="Method", option=methods })
	local remotehost = cfe({ value=entryname, label="Remote Host" })
	local remotemailbox = cfe({ label="Remote Mailbox" })
	local remotepassword = cfe({ label="Password" })
	local localhost = cfe({ label="Local Host" })
	local localmailbox = cfe({ label="Local Mailbox" })
	local localdomain = cfe({ label="Local Domain" })
	local ssl = cfe({ type="boolean", value=false, label="SSL Encryption" })
	local envelope = cfe({ type="select", value="X-Envelope-To", label="Envelope Mode", option={"X-Original-To", "Delivered-To", "X-Envelope-To", "Received", "disabled"} })

	local entry = findentryline(entryname, meth, remotemailbx, localdom)
	if entry then
		if entry[1] == "skip" then
			enabled.value = false
		end
		local reverseentry = findkeywordsinentry(entry)
		if reverseentry["local"] or reverseentry["localdomains"] then
			localdomain.value = entry[(reverseentry["local"] or reverseentry["localdomains"])+1] or localdomain.value
			method.value = "pop3domain"
		elseif reverseentry["proto"] or reverseentry["protocol"] then
			method.value = entry[(reverseentry["proto"] or reverseentry["protocol"])+1] or method.value
		end
		if reverseentry["user"] or reverseentry["username"] then
			remotemailbox.value = entry[(reverseentry["user"] or reverseentry["username"])+1] or remotemailbox.value
		end
		if reverseentry["pass"] or reverseentry["password"] then
			remotepassword.value = entry[(reverseentry["pass"] or reverseentry["password"])+1] or remotepassword.value
		end
		if reverseentry["smtphost"] then
			localhost.value = entry[reverseentry["smtphost"]+1] or localhost.value
		end
		if reverseentry["is"] then
			localmailbox.value = entry[reverseentry["is"]+1] or localmailbox.value
		end
		if reverseentry["fetchdomains"] then
			localdomain.value = entry[reverseentry["fetchdomains"]+1] or localdomain.value
		end
		if reverseentry["ssl"] then
			ssl.value = true
		end
		if reverseentry["envelope"] then
			if entry[reverseentry["envelope"]-1] == "no" then
				envelope.value = "disabled"
			elseif validator.is_integer(entry[reverseentry["envelope"]+1] or "") then
				envelope.value = entry[reverseentry["envelope"]+2]
			else
				envelope.value = entry[reverseentry["envelope"]+1]
			end
		end
	end

	return cfe({ type="group", value={enabled=enabled, method=method, remotehost=remotehost, remotemailbox=remotemailbox, remotepassword=remotepassword, localhost=localhost, localmailbox=localmailbox, localdomain=localdomain, ssl=ssl, envelope=envelope}, label="Fetchmail Entry" })
end

function updateentry(self, entrystruct)
	local success, entrystruct = validateentry(entrystruct)
	local entry = findentryline(entrystruct.value.remotehost.value, entrystruct.value.method.value, entrystruct.value.remotemailbox.value, entrystruct.value.localdomain.value)
	if not entry then
		entrystruct.value.remotehost.errtxt = "Entry not found"
		success = false
	end

	if success then
		writeentryline(entrystruct, entry)
	else
		entrystruct.errtxt = "Failed to update entry"
	end

	return entrystruct
end

function createentry(self, entrystruct)
	local success, entrystruct = validateentry(entrystruct)
	local entry = findentryline(entrystruct.value.remotehost.value, entrystruct.value.method.value, entrystruct.value.remotemailbox.value, entrystruct.value.localdomain.value)
	if entry then
		entrystruct.value.remotehost.errtxt = "Entry already exists"
		success = false
	end

	if success then
		writeentryline(entrystruct)
	else
		entrystruct.errtxt = "Failed to create entry"
	end

	return entrystruct
end

function get_deleteentry(self, clientdata)
	local retval = {}
	retval.remotehost = cfe({ value=clientdata.remotehost or "", label="Remote Host" })
	retval.method = cfe({ type="select", value=clientdata.method or "pop3", label="Method", option=methods })
	retval.remotemailbox = cfe({ value=clientdata.remotemailbox or "", label="Remote Mailbox" })
	retval.localdomain = cfe({ value=clientdata.localdomain or "", label="Local Domain" })

	return cfe({ type="group", value=retval, label="Delete Entry" })
end

function deleteentry(self, ent)
	local entry = findentryline(ent.value.remotehost.value, ent.value.method.value, ent.value.remotemailbox.value, ent.value.localdomain.value)
	if entry then
		writeentryline(nil, entry)
	else
		ent.errtxt = "Failed to delete entry - not found"
	end

	return ent
end