module(..., package.seeall) -- Load libraries require("modelfunctions") require("posix") require("fs") require("format") 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)+1, endword-1) 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") deleteoption("ssl") 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 if entrystruct.value.ssl.value and not reverseentry["ssl"] then entryline[table.maxn(entryline)+1] = "ssl" elseif not entrystruct.value.ssl.value and reverseentry["ssl"] then entryline[reverseentry["ssl"]] = nil 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 -f "..configfile.." 2>&1" elseif action:lower() == "test" then cmd = "/usr/bin/fetchmail -d0 -v -k -f "..configfile.." 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) -- FIXME - validation local retval = modelfunctions.setfiledetails(filedetails, {configfile}) 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 = format.parse_ini_file(fs.read_file(confdfile), "", "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(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 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)) 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 ssl = cfe({ type="boolean", value=false, label="SSL Encryption" }) 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 = 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 end return cfe({ type="group", value={enabled=enabled, method=method, remotehost=remotehost, remotemailbox=remotemailbox, remotepassword=remotepassword, localhost=localhost, localmailbox=localmailbox, localdomain=localdomain, ssl=ssl}, 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