blob: 49dc44ec06587e7090e4e9d4c595310c03d6e847 [file] [log] [blame]
--[[
Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
]]--
-- This is preferences.lua - an account info agent
local JSON = require 'cjson'
local elastic = require 'lib/elastic'
local user = require 'lib/user'
local cross = require 'lib/cross'
local smtp = require 'socket.smtp'
local config = require 'lib/config'
local aaa = require 'lib/aaa'
local utils = require 'lib/utils'
--[[
Remove nulls values from a table
This is for use in tidying up account.credentials.altemail
which may contain null entries.
Rather than continually check for them, remove them from
the input before use.
]]
local function filtertable(input)
-- table.remove can affect pairs()
-- so repeat until no more to do
repeat
local isClean = true
for k, v in pairs(input) do
if not v or v == JSON.null then
table.remove(input, k)
isClean = false
break
end
end
until isClean
end
--[[
Get login details (if logged in), mail list counts and descriptions
Parameters: (cookie required)
- logout: Whether to log out of the system (optional)
- associate=$email - associate the account with the $email address
- verify&hash=$hash - verify an association request $hash
- removealt=$email - remove an alternate $email address
- save - save preferences as specified (does not merge)
- addfav=$list - add a favourite $list
- remfav=$list - remove a favourite $list
]]--
function handle(r)
cross.contentType(r, "application/json")
local DEBUG = config.debug or false
local START = DEBUG and r:clock() or nil
local get = r:parseargs()
local login = {
loggedIn = false
}
local prefs = nil -- Default to JS prefs if not logged in
-- prefs?
local account = user.get(r)
-- while we're here, are you logging out?
if get.logout and account then
user.logout(r, account)
r:puts[[{"logout": true}]]
return cross.OK
end
-- associating an email address??
if get.associate and account and get.associate:match("^%S+@%S+$") then
local fp, lp = get.associate:match("([^@]+)@([^@]+)")
if config.no_association then
for k, v in pairs(config.no_association) do
if r.strcmp_match(lp:lower(), v) or v == "*" then
r:puts(JSON.encode{error="You cannot associate email addresses from this domain"})
return cross.OK
end
end
end
if get.associate == account.credentials.email then
r:puts(JSON.encode{error="The primary mail address cannot be added as an alternate"})
return cross.OK
end
account.credentials.altemail = account.credentials.altemail or {}
filtertable(account.credentials.altemail)
local duplicateRequest = false
for k, v in pairs(account.credentials.altemail) do
if v.email == get.associate then -- duplicate request
if v.verified then -- already exists
r:puts(JSON.encode{error="That email is already defined as an alternate"})
-- OK to return here as we don't need to update anything
return cross.OK
else -- pending verification, update the hash
v.hash = hash -- update all pending requests to the new hash
-- cannot return here in case there are multiple entries
-- also we need to mail the new hash to the user
duplicateRequest = true
end
end
end
local hash = r:md5(math.random(1,999999) .. os.time() .. account.cid)
local scheme = "https"
if r.port == 80 then
scheme = "http"
end
local domain = ("%s://%s:%u/"):format(scheme, r.hostname, r.port)
if r.headers_in['Referer'] and r.headers_in['Referer']:match("merge%.html") then
domain = r.headers_in['Referer']:gsub("/merge%.html", "/")
end
local vURL = ("%sapi/preferences.lua?verify=true&hash=%s"):format(domain, hash)
local mldom = r.headers_in['Referer'] and r.headers_in['Referer']:match("https?://([^/:]+)") or r.hostname
if not mldom then mldom = r.hostname end
-- send email
local source = smtp.message{
headers = {
subject = "Confirm email address association in Pony Mail",
to = get.associate,
from = ("\"Pony Mail\"<no-reply@%s>"):format(mldom)
},
body = ([[
You (or someone else) has requested to associate the email address '%s' with the account '%s' in Pony Mail.
If you wish to complete this association, please visit
%s
whilst logged in to Pony Mail.
Note: if you have repeated the association request, only the last URL will work.
...Or if you didn't request this, just ignore this email.
With regards,
Pony Mail - Email for Ponies and People.
]]):format(get.associate, account.credentials.email, vURL)
}
-- send email!
local rv, er = smtp.send{
from = ("\"Pony Mail\"<no-reply@%s>"):format(r.hostname),
rcpt = get.associate,
source = source,
server = config.mailserver,
port = config.mailport or nil -- if not specified, use the default
}
-- only update the account if the mail was sent OK
if rv then
if not duplicateRequest then
table.insert(account.credentials.altemail, { email = get.associate, hash = hash, verified = false})
end
user.save(r, account, true)
end
r:puts(JSON.encode{requested = rv or er})
return cross.OK
end
-- verify alt email?
if get.verify and get.hash and account and account.credentials.altemail then
filtertable(account.credentials.altemail)
local verified = false
for k, v in pairs(account.credentials.altemail) do
if v.hash == get.hash then
account.credentials.altemail[k].verified = true
account.credentials.altemail[k].hash = nil
verified = true
-- fix all the matches
end
end
user.save(r, account, true)
-- response goes back to the browser direct
cross.contentType(r, "text/plain")
if verified then
r:puts("Email address verified! Thanks for shopping at Pony Mail!\n")
else
r:puts("Either you supplied an invalid hash or something else went wrong.\n")
end
return cross.OK
end
-- remove alt email?
if get.removealt and account and account.credentials.altemail then
filtertable(account.credentials.altemail)
for k, v in pairs(account.credentials.altemail) do
if v.email == get.removealt then
table.remove(account.credentials.altemail, k)
break
end
end
user.save(r, account, true)
r:puts(JSON.encode{removed = true})
return cross.OK
end
-- Or are you saving your preferences?
if get.save and account then
prefs = {}
for k, v in pairs(get) do
if k ~= 'save' then
prefs[k] = v
end
end
account.preferences = prefs
user.save(r, account)
r:puts[[{"saved": true}]]
return cross.OK
end
-- Adding a favorite list
if get.addfav and account then
local add = get.addfav
local favs = account.favorites or {}
local found = false
-- ensure it's not already there....
for k, v in pairs(favs) do
if v == add then
found = true
break
end
end
-- if not found, add it
if not found then
table.insert(favs, add)
end
-- save prefs
account.favorites = favs
user.favs(r, account)
r:puts[[{"saved": true}]]
return cross.OK
end
-- Removing a favorite list
if get.remfav and account then
local rem = get.remfav
local favs = account.favorites or {}
-- ensure it's here....
for k, v in pairs(favs) do
if v == rem then
table.remove(favs, k)
break
end
end
-- save prefs
account.favorites = favs
user.favs(r, account)
r:puts[[{"saved": true}]]
return cross.OK
end
-- don't allow failed options to drop-thru
for _, v in pairs({'associate', 'verify', 'removealt', 'save', 'addfav', 'remfav'}) do
if get[v] then
if not account then
r:puts(JSON.encode{error="Not logged in"})
else
r:puts(JSON.encode{error="Missing or invalid parameter(s)"})
end
return cross.OK
end
end
-- Get list counts (cached if possible)
local NOWISH = math.floor(os.time() / 600)
local PM_LISTS_KEY = "pm_lists_counts_" .. r.hostname .. "-" .. NOWISH
local cache = r:ivm_get(PM_LISTS_KEY)
local listcounts = {} -- summary of aggregated data for cache
if cache then
listcounts = JSON.decode(cache)
else
-- aggregate the documents by listname, privacy flag, recent docs
local alldocs = elastic.raw{
size = 0, -- we don't need the hits themselves
aggs = {
listnames = {
terms = {
field = "list_raw",
size = utils.MAX_LIST_COUNT
},
aggs = {
-- split list into public and private buckets
privacy = {
terms = {
field = "private"
},
aggs = {
-- Create a single bucket of recent mails
recent = {
range = {
field = "date",
ranges = { {from = "now-90d"} }
}
}
}
}
}
}
}
}
-- squash the output for caching (it's quite verbose otherwise)
for _, entry in pairs (alldocs.aggregations.listnames.buckets) do
local listname = entry.key:lower()
listcounts[listname] = {}
-- the same list may have both private and public docs
for _, privacy in pairs(entry.privacy.buckets) do
listcounts[listname][privacy.key_as_string] = privacy.recent.buckets[1].doc_count
end
end
-- save the squashed counts in cache
r:ivm_set(PM_LISTS_KEY, JSON.encode(listcounts))
end
-- Now count the docs and lists that are visible to the current user
local lists = {}
for listname, entry in pairs(listcounts) do
local _, list, domain = aaa.parseLid(listname)
-- Note: the default implementation ensures that list and domain are non-empty
-- Check lengths just in case a local version does not do so
if list and domain and #list > 0 and #domain > 0 then
-- there may be both private and public docs in the list
for privacy, recent_count in pairs(entry) do
local isPublic = privacy == 'false'
-- does the user have access to this list?
if isPublic or aaa.canAccessList(r, listname, account) then
-- create the domain entry if necessary
lists[domain] = lists[domain] or {}
-- check if we have a list entry yet
if lists[domain][list] then
lists[domain][list] = lists[domain][list] + recent_count
else
lists[domain][list] = recent_count -- init the entry
end
end
end
end
end
-- do we need to remove junk?
if config.listsDisplay then
for k, v in pairs(lists) do
if not k:match(config.listsDisplay) then
lists[k] = nil
end
end
end
-- Get notifs
local notifications = 0
if account then
local _, notifs = pcall(function() return elastic.find("seen:0 AND recipient:" .. r:sha1(account.cid), 10, "notifications") end)
if notifs and #notifs > 0 then
notifications = #notifs
end
end
account = account or {}
local stat, descs = pcall(function() return elastic.find("*", 9999, "mailinglists", "name") end)
if not stat or not descs then
descs = {} -- ensure descs is valid
end
-- try to extrapolate foo@bar.tld here
for k, v in pairs(descs) do
local _, l, d = aaa.parseLid(v.list:lower())
if l and d then
descs[k].lid = ("%s@%s"):format(l, d)
else
descs[k].lid = v.list
end
end
local alts = {}
if account and account.credentials and type(account.credentials.altemail) == "table" then
filtertable(account.credentials.altemail)
for k, v in pairs(account.credentials.altemail) do
if v.verified then
table.insert(alts, v.email)
end
end
end
r:puts(JSON.encode{
lists = lists,
descriptions = descs,
preferences = account.preferences,
login = {
favorites = account.favorites,
credentials = account.credentials,
notifications = notifications,
alternates = alts
},
took = DEBUG and (r:clock() - START) or nil
})
return cross.OK
end
cross.start(handle)