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
|
module(..., package.seeall)
-- Load libraries
require("modelfunctions")
require("posix")
require("fs")
require("getopts")
require("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 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), endword)
end
table.insert(retval[#retval], word)
offset = endword + 1
end
end
end
return retval
end
local function findentryline(entryname)
if entryname and entryname ~= "" then
config = config or parseconfigfile(fs.read_file(configfile))
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
return entry
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
entryline = entryline or {}
insertentries = {}
local reverseentry = {}
for i,word in ipairs(entryline) do reverseentry[word] = i end
-- Server options must come before user options, so add user option to end
-- and add server options just after remotehost (3). To do this, we'll use an array
-- of insertentries that will be inserted at the end. To delete entries, just set
-- them to nil, creating a sparse array. We can fix that at the end also
-- Here are two helper functions
function setserveroption(option, value)
if reverseentry[option] then
entryline[reverseentry[option]+1] = value
else
insertentries[table.maxn(insertentries)+1] = option
insertentries[table.maxn(insertentries)+1] = value
end
end
function setuseroption(option, value)
if reverseentry[option] then
entryline[reverseentry[option]+1] = value
else
entryline[table.maxn(entryline)+1] = option
entryline[table.maxn(entryline)+1] = value
end
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
if entrystruct.value.enabled.value then
entryline[1] = "poll"
else
entryline[1] = "skip"
end
entryline[2] = entrystruct.value.remotehost.value
-- generic stuff
deleteoptionandvalue("proto")
deleteoptionandvalue("local")
deleteoptionandvalue("user")
deleteoptionandvalue("pass")
-- remove here and there
for i=1,table.maxn(entryline) do
if entryline[i] == "here" or entryline[i] == "there" then
entryline[i] = nil
end
end
if entrystruct.value.method.value == "pop3domain" then
setserveroption("protocol", "pop3")
setserveroption("localdomains", entrystruct.value.localdomain.value)
deleteoptionandvalue("is")
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("is")
deleteoptionandvalue("to")
deleteoptionandvalue("smtpaddress")
setuseroption("fetchdomains", entrystruct.value.localdomain.value)
else -- Method not pop3domain or etrn
setserveroption("protocol", entrystruct.value.method.value)
deleteoptionandvalue("localdomains")
setuseroption("is", entrystruct.value.localmailbox.value)
deleteoptionandvalue("to")
deleteoptionandvalue("smtpaddress")
deleteoptionandvalue("fetchdomains")
end
if entrystruct.value.method.value == "etrn" then
deletenooption("dns")
deleteoptionandvalue("username")
deleteoptionandvalue("password")
deleteoptionandvalue("smtphost")
deletenooption("rewrite")
deleteoption("fetchall")
else -- Method not etrn
if not reverseentry["dns"] then
insertentries[table.maxn(insertentries)+1] = "no"
insertentries[table.maxn(insertentries)+1] = "dns"
end
setuseroption("username", entrystruct.value.remotemailbox.value)
setuseroption("password", '"'..entrystruct.value.remotepassword.value..'"')
setuseroption("smtphost", entrystruct.value.localhost.value)
if not reverseentry["rewrite"] then
entryline[table.maxn(entryline)+1] = "no"
entryline[table.maxn(entryline)+1] = "rewrite"
end
if not reverseentry["fetchall"] then
entryline[table.maxn(entryline)+1] = "fetchall"
end
end
-- Now, insert the insertentries and remove the nil entries
table.insert(insertentries, 1, entryline[1])
table.insert(insertentries, 2, entryline[2])
for i=3,table.maxn(entryline) do
table.insert(insertentries, entryline[i])
end
insertentries.linenum = entryline.linenum
entryline = insertentries
end
local file = fs.read_file(configfile)
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, "rwx--x---")
config = nil
end
local function validateentry(entry)
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
local success = true
success = modelfunctions.validateselect(entry.value.method) 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 startstop_service(action)
local cmd
if action:lower() == "run" then
cmd = "/usr/bin/fetchmail -d0 -v 2>&1"
elseif action:lower() == "test" then
cmd = "/usr/bin/fetchmail -d0 -v -k 2>&1"
else
return modelfunctions.startstop_service(processname, action)
end
local f = io.popen(cmd)
cmdresult = f:read("*a")
f:close()
return cfe({ value=cmdresult, label="Start/Stop result" })
end
function getstatus()
return modelfunctions.getstatus(processname, packagename, "Fetchmail Status")
end
function get_filedetails()
-- FIXME - validation
return modelfunctions.getfiledetails(configfile)
end
function update_filecontent(filedetails)
filedetails.value.filename.value = configfile
-- FIXME - validation
local retval = modelfunctions.setfiledetails(filedetails)
posix.chmod(configfile, "rwx--x---")
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))
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 = getopts.getoptsfromfile(confdfile, "", "polling_period")
if confd then
interval.value = confd
end
return cfe({ type="group", value={interval=interval, postmaster=postmaster, bounceerrors=bounceerrors}, label="Fetchmail Global Config" })
end
function updateconfig(conf)
local success, conf = validateconfig(conf)
if success then
local file = fs.read_file(configfile)
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
line = "set postmaster "..conf.value.postmaster.value.."\n"
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, "rwx--x---")
config = nil
getopts.setoptsinfile(confdfile, "", "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))
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 = {}
for i,word in ipairs(entry) do reverseentry[word] = i end
local method = "error"
if reverseentry["local"] or reverseentry["localdomains"] then
method = "pop3domain"
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
table.insert(entries.value, {entry=entry[2], method=method, enabled=enabled})
end
end
return entries
end
function readentry(entryname)
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 entry = findentryline(entryname)
if entry then
if entry[1] == "skip" then
enabled.value = false
end
local reverseentry = {}
for i,word in ipairs(entry) do reverseentry[word] = i end
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 = string.sub(entry[(reverseentry["pass"] or reverseentry["password"])+1] or "", 2, -2) 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
end
return cfe({ type="group", value={enabled=enabled, method=method, remotehost=remotehost, remotemailbox=remotemailbox, remotepassword=remotepassword, localhost=localhost, localmailbox=localmailbox, localdomain=localdomain}, label="Fetchmail Entry" })
end
function updateentry(entrystruct)
local success, entrystruct = validateentry(entrystruct)
local entry = findentryline(entrystruct.value.remotehost.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(entrystruct)
local success, entrystruct = validateentry(entrystruct)
local entry = findentryline(entrystruct.value.remotehost.value)
if entry then
entrystruct.value.remotehost.errtxt = "Entry already exists"
success = false
end
if success then
writeentryline(entrystruct)
else
entrystruct.errtxt = "Failed to update entry"
end
return entrystruct
end
function deleteentry(entryname)
local retval = cfe({ value="Deleted entry", label="Delete Fetchmail Entry Result" })
local entry = findentryline(entryname)
if entry then
writeentryline(nil, entry)
else
retval.value = "Failed to delete entry - not found"
end
return retval
end
|