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", } -- ################################################################################ -- 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 {} if entrystruct.value.enabled.value then entryline[1] = "poll" else entryline[1] = "skip" end entryline[2] = entrystruct.value.remotehost.value 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) -- start with the user options so we can add to the end and not mess up reverseentry -- this means that we do method last if reverseentry["user"] or reverseentry["username"] then entryline[(reverseentry["user"] or reverseentry["username"])+1] = entrystruct.value.remotemailbox.value else entryline[#entryline+1] = "username" entryline[#entryline+1] = entrystruct.value.remotemailbox.value end if reverseentry["pass"] or reverseentry["password"] then entryline[(reverseentry["pass"] or reverseentry["password"])+1] = '"'..entrystruct.value.remotepassword.value..'"' else entryline[#entryline+1] = "password" entryline[#entryline+1] = '"'..entrystruct.value.remotepassword.value..'"' end if reverseentry["smtphost"] then entryline[reverseentry["smtphost"]+1] = entrystruct.value.localhost.value else entryline[#entryline+1] = "smtphost" entryline[#entryline+1] = entrystruct.value.localhost.value end -- add in some user options if not reverseentry["rewrite"] then entryline[#entryline+1] = "no" entryline[#entryline+1] = "rewrite" end if not reverseentry["fetchall"] then entryline[#entryline+1] = "fetchall" end -- now handle the method, localmailbox, and localdomain if entrystruct.value.method.value == "pop3domain" then if reverseentry["to"] then entryline[reverseentry["to"]+1] = "*" else entryline[#entryline+1] = "to" entryline[#entryline+1] = "*" end if reverseentry["smtpaddress"] then entryline[reverseentry["smtpaddress"]+1] = entrystruct.value.localdomain.value else entryline[#entryline+1] = "smtpaddress" entryline[#entryline+1] = entrystruct.value.localdomain.value end if reverseentry["is"] then -- THIS MESSES UP reverseentry FOR THE USER OPTIONS table.remove(entryline, reverseentry["is"]) table.remove(entryline, reverseentry["is"]) end if reverseentry["proto"] or reverseentry["protocol"] then entryline[(reverseentry["proto"] or reverseentry["protocol"])+1] = "pop3" else -- THIS MESSES UP reverseentry FOR EVERYTHING table.insert(entryline, 3, "pop3") table.insert(entryline, 3, "protocol") -- FIX reverseentry reverseentry = {} for i,word in ipairs(entryline) do reverseentry[word] = i end end if reverseentry["local"] or reverseentry["localdomains"] then entryline[(reverseentry["local"] or reverseentry["localdomains"])+1] = entrystruct.value.localdomain.value else -- THIS MESSES UP reverseentry FOR EVERYTHING table.insert(entryline, 3, entrystruct.value.localdomain.value) table.insert(entryline, 3, "localdomains") end if not reverseentry["dns"] then -- THIS MESSES UP reverseentry FOR EVERYTHING table.insert(entryline, 3, "dns") table.insert(entryline, 3, "no") end else if reverseentry["is"] then entryline[reverseentry["is"]+1] = entrystruct.value.localmailbox.value else entryline[#entryline+1] = "is" entryline[#entryline+1] = entrystruct.value.localmailbox.value end if reverseentry["to"] then -- THIS MESSES UP reverseentry FOR THE USER OPTIONS table.remove(entryline, reverseentry["to"]) table.remove(entryline, reverseentry["to"]) -- FIX reverseentry reverseentry = {} for i,word in ipairs(entryline) do reverseentry[word] = i end end if reverseentry["smtpaddress"] then -- THIS MESSES UP reverseentry FOR THE USER OPTIONS table.remove(entryline, reverseentry["smtpaddress"]) table.remove(entryline, reverseentry["smtpaddress"]) end if reverseentry["proto"] or reverseentry["protocol"] then entryline[(reverseentry["proto"] or reverseentry["protocol"])+1] = entrystruct.value.method.value else -- THIS MESSES UP reverseentry FOR EVERYTHING table.insert(entryline, 3, entrystruct.value.method.value) table.insert(entryline, 3, "protocol") -- FIX reverseentry reverseentry = {} for i,word in ipairs(entryline) do reverseentry[word] = i end end if reverseentry["local"] or reverseentry["localdomains"] then -- THIS MESSES UP reverseentry FOR EVERYTHING table.remove(entryline, (reverseentry["local"] or reverseentry["localdomains"])) table.remove(entryline, (reverseentry["local"] or reverseentry["localdomains"])) end if not reverseentry["dns"] then -- THIS MESSES UP reverseentry FOR EVERYTHING table.insert(entryline, 3, "dns") table.insert(entryline, 3, "no") end end -- remove here and there for i=#entryline,1,-1 do if entryline[i] == "here" or entryline[i] == "there" then table.remove(entryline, i) end end 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) local success = true success = modelfunctions.validateselect(entry.value.method) and success if entry.value.remotehost.value == "" then entry.value.remotehost.errtxt = "Invalid entry - cannot be blank" success = false elseif string.find(entry.value.remotehost.value, "[^%w.-]") then entry.value.remotehost.errtxt = "Invalid entry - may only contain alphanumeric, '.', or '-'" success = false end if entry.value.remotemailbox.value == "" then entry.value.remotemailbox.errtxt = "Invalid entry - cannot be blank" success = false elseif string.find(entry.value.remotemailbox.value, "[^%w.-_@]") then entry.value.remotemailbox.errtxt = "Invalid entry" success = false end if entry.value.remotepassword.value == "" then entry.value.remotepassword.errtxt = "Invalid entry - cannot be blank" success = false elseif string.find(entry.value.remotepassword.value, "%s") then entry.value.remotepassword.errtxt = "Invalid entry - cannot contain whitespace" success = false end if entry.value.localhost.value == "" then entry.value.localhost.errtxt = "Invalid entry - cannot be blank" success = false elseif string.find(entry.value.localhost.value, "[^%w.-]") then entry.value.localhost.errtxt = "Invalid entry - may only contain alphanumeric, '.', or '-'" success = false end if entry.value.method.value == "pop3domain" and entry.value.localmailbox.value ~= "" then entry.value.localmailbox.errtxt = "Cannot define local mailbox for pop3domain method" success = false elseif entry.value.method.value ~= "pop3domain" and entry.value.localmailbox.value == "" then entry.value.localmailbox.errtxt = "Invalid entry - cannot be blank" success = false elseif string.find(entry.value.localmailbox.value, "[^%w.-_@]") then entry.value.localmailbox.errtxt = "Invalid entry" success = false end if entry.value.method.value ~= "pop3domain" and entry.value.localdomain.value ~= "" then entry.value.localdomain.errtxt = "Cannot define local domain unless pop3domain method" success = false elseif entry.value.method.value == "pop3domain" and entry.value.localdomain.value == "" then entry.value.localdomain.errtxt = "Invalid entry - cannot be blank" success = false elseif string.find(entry.value.localdomain.value, "[^%w.-]") then entry.value.localdomain.errtxt = "Invalid entry - may only contain alphanumeric, '.', or '-'" success = false 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 retvel = 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 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