├── test ├── resources │ ├── fail │ │ ├── empty.mbox │ │ └── NoList.mbox │ ├── pass │ │ ├── nodate.mbox │ │ ├── attachment2.mbox │ │ ├── 00439.982a2ff6189badfe70c2fe3c972466a2.mbox │ │ ├── minimal.mbox │ │ ├── 00439.982a2ff6189badfe70c2fe3c972466a2_fixcharset.mbox │ │ ├── emptyattach2.mbox │ │ ├── text-enriched.mbox │ │ └── emptybody.mbox │ ├── roundtrip │ │ └── mod-openoffice-users-de-201612-6891.mbox │ └── valid │ │ └── test_288.mbox ├── requirements.txt ├── mock_r.lua ├── import_test.sh ├── luasocket_test.lua ├── stats_cli.lua └── generatortest.py ├── site ├── favicon.ico ├── images │ ├── arrow.png │ ├── atom.png │ ├── demo.png │ ├── logo.png │ ├── quote.png │ ├── user.png │ ├── private.png │ ├── search.png │ ├── spinner.gif │ ├── themes.png │ ├── attachment.png │ ├── daterange.ico │ ├── download.png │ ├── logo_large.png │ ├── opensearch.png │ ├── user_notif.png │ ├── demo_trends.png │ ├── oauth_apache.png │ ├── oauth_github.png │ ├── oauth_google.png │ ├── oauth_online.png │ ├── unknown_pony.png │ ├── oauth_internal.png │ ├── oauth_twitter.png │ ├── treeview_child.png │ ├── treeview_none.png │ ├── treeview_parent.png │ ├── user_loggedout.png │ └── treeview_lastchild.png ├── fonts │ ├── glyphicons-halflings-regular.eot │ ├── glyphicons-halflings-regular.ttf │ ├── glyphicons-halflings-regular.woff │ └── glyphicons-halflings-regular.woff2 ├── js │ ├── dev │ │ ├── combine.sh │ │ ├── ponymail_favorites.js │ │ ├── ponymail_seeders.js │ │ ├── ponymail_stats.js │ │ ├── ponymail_assign_vars.js │ │ ├── ponymail_timetravel.js │ │ └── ponymail_zzz.js │ ├── alts.js │ ├── config.js.sample │ └── weburl.js ├── api │ ├── websearch.lua │ ├── lib │ │ ├── trace.lua │ │ ├── config.lua.sample │ │ ├── cross.lua │ │ ├── utils.lua │ │ └── user.lua │ ├── source.lua │ ├── notifications.lua │ ├── thread.lua │ └── email.lua ├── oauth.html ├── permalink.html ├── ngrams.html ├── trends.html ├── notifications.html ├── css │ ├── solar.css │ └── metro.css ├── index.html └── thread.html ├── test_one.py ├── NOTICE ├── configs ├── ponymail_nginx.conf └── ponymail_httpd.conf ├── requirements.txt ├── dockerfiles ├── ponymail_httpd_docker.conf └── debian │ └── Dockerfile ├── DISCLAIMER ├── .gitignore ├── STATUS ├── aaa_examples ├── README.md ├── aaa_by_portal.lua ├── aaa_with_subgroups.lua ├── aaa_by_email_address.lua └── aaa_with_ldap.lua ├── tools ├── ponymailconfig.py ├── json_tidy.py ├── update-asf-lists.sh ├── ponymail.cfg.sample ├── import-all-asf-lists.sh ├── trace.py ├── nullfav.py ├── email_utils_patch.py ├── push-failures.py ├── feedwrapper.py ├── install.py ├── list-lists.py ├── mboxo_patch.py ├── copy-list.py └── missing.py ├── .github └── workflows │ └── pythonpackage.yml └── RELEASE-NOTES.md /test/resources/fail/empty.mbox: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/resources/fail/NoList.mbox: -------------------------------------------------------------------------------- 1 | From 2 | -------------------------------------------------------------------------------- /site/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apache/incubator-ponymail/HEAD/site/favicon.ico -------------------------------------------------------------------------------- /test/resources/pass/nodate.mbox: -------------------------------------------------------------------------------- 1 | From 2 | List-Id: 3 | 4 | No date 5 | 6 | -------------------------------------------------------------------------------- /site/images/arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apache/incubator-ponymail/HEAD/site/images/arrow.png -------------------------------------------------------------------------------- /site/images/atom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apache/incubator-ponymail/HEAD/site/images/atom.png -------------------------------------------------------------------------------- /site/images/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apache/incubator-ponymail/HEAD/site/images/demo.png -------------------------------------------------------------------------------- /site/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apache/incubator-ponymail/HEAD/site/images/logo.png -------------------------------------------------------------------------------- /site/images/quote.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apache/incubator-ponymail/HEAD/site/images/quote.png -------------------------------------------------------------------------------- /site/images/user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apache/incubator-ponymail/HEAD/site/images/user.png -------------------------------------------------------------------------------- /test_one.py: -------------------------------------------------------------------------------- 1 | # Dummy test 2 | 3 | import pytest 4 | def test_file1_method1(): 5 | print("Hello") -------------------------------------------------------------------------------- /site/images/private.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apache/incubator-ponymail/HEAD/site/images/private.png -------------------------------------------------------------------------------- /site/images/search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apache/incubator-ponymail/HEAD/site/images/search.png -------------------------------------------------------------------------------- /site/images/spinner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apache/incubator-ponymail/HEAD/site/images/spinner.gif -------------------------------------------------------------------------------- /site/images/themes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apache/incubator-ponymail/HEAD/site/images/themes.png -------------------------------------------------------------------------------- /site/images/attachment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apache/incubator-ponymail/HEAD/site/images/attachment.png -------------------------------------------------------------------------------- /site/images/daterange.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apache/incubator-ponymail/HEAD/site/images/daterange.ico -------------------------------------------------------------------------------- /site/images/download.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apache/incubator-ponymail/HEAD/site/images/download.png -------------------------------------------------------------------------------- /site/images/logo_large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apache/incubator-ponymail/HEAD/site/images/logo_large.png -------------------------------------------------------------------------------- /site/images/opensearch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apache/incubator-ponymail/HEAD/site/images/opensearch.png -------------------------------------------------------------------------------- /site/images/user_notif.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apache/incubator-ponymail/HEAD/site/images/user_notif.png -------------------------------------------------------------------------------- /site/images/demo_trends.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apache/incubator-ponymail/HEAD/site/images/demo_trends.png -------------------------------------------------------------------------------- /site/images/oauth_apache.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apache/incubator-ponymail/HEAD/site/images/oauth_apache.png -------------------------------------------------------------------------------- /site/images/oauth_github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apache/incubator-ponymail/HEAD/site/images/oauth_github.png -------------------------------------------------------------------------------- /site/images/oauth_google.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apache/incubator-ponymail/HEAD/site/images/oauth_google.png -------------------------------------------------------------------------------- /site/images/oauth_online.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apache/incubator-ponymail/HEAD/site/images/oauth_online.png -------------------------------------------------------------------------------- /site/images/unknown_pony.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apache/incubator-ponymail/HEAD/site/images/unknown_pony.png -------------------------------------------------------------------------------- /site/images/oauth_internal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apache/incubator-ponymail/HEAD/site/images/oauth_internal.png -------------------------------------------------------------------------------- /site/images/oauth_twitter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apache/incubator-ponymail/HEAD/site/images/oauth_twitter.png -------------------------------------------------------------------------------- /site/images/treeview_child.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apache/incubator-ponymail/HEAD/site/images/treeview_child.png -------------------------------------------------------------------------------- /site/images/treeview_none.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apache/incubator-ponymail/HEAD/site/images/treeview_none.png -------------------------------------------------------------------------------- /site/images/treeview_parent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apache/incubator-ponymail/HEAD/site/images/treeview_parent.png -------------------------------------------------------------------------------- /site/images/user_loggedout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apache/incubator-ponymail/HEAD/site/images/user_loggedout.png -------------------------------------------------------------------------------- /site/images/treeview_lastchild.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apache/incubator-ponymail/HEAD/site/images/treeview_lastchild.png -------------------------------------------------------------------------------- /test/resources/pass/attachment2.mbox: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apache/incubator-ponymail/HEAD/test/resources/pass/attachment2.mbox -------------------------------------------------------------------------------- /site/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apache/incubator-ponymail/HEAD/site/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /site/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apache/incubator-ponymail/HEAD/site/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /site/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apache/incubator-ponymail/HEAD/site/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /site/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apache/incubator-ponymail/HEAD/site/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /test/resources/pass/00439.982a2ff6189badfe70c2fe3c972466a2.mbox: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apache/incubator-ponymail/HEAD/test/resources/pass/00439.982a2ff6189badfe70c2fe3c972466a2.mbox -------------------------------------------------------------------------------- /test/resources/pass/minimal.mbox: -------------------------------------------------------------------------------- 1 | From 2 | List-Id: 3 | Date: Thu, 17 Nov 2016 00:49:27 +0000 (UTC) 4 | From: test@test.invalid 5 | 6 | Currently this needs a body ... 7 | 8 | -------------------------------------------------------------------------------- /test/resources/roundtrip/mod-openoffice-users-de-201612-6891.mbox: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apache/incubator-ponymail/HEAD/test/resources/roundtrip/mod-openoffice-users-de-201612-6891.mbox -------------------------------------------------------------------------------- /test/resources/pass/00439.982a2ff6189badfe70c2fe3c972466a2_fixcharset.mbox: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apache/incubator-ponymail/HEAD/test/resources/pass/00439.982a2ff6189badfe70c2fe3c972466a2_fixcharset.mbox -------------------------------------------------------------------------------- /test/requirements.txt: -------------------------------------------------------------------------------- 1 | # for pytest/unit tests 2 | PyYAML~=5.3.1 3 | elasticsearch-dsl>=5.0.0 4 | elasticsearch~=5.0.0 5 | certifi~=2020.6.20 6 | chardet~=3.0.4 7 | netaddr~=0.8.0 8 | formatflowed~=2.0.0 9 | html2text~=2019.8.11 10 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Apache Pony Mail (Incubating) 2 | Copyright 2015-2020 The Apache Software Foundation. 3 | 4 | This product includes software developed at 5 | The Apache Software Foundation (http://www.apache.org/). 6 | 7 | Portions of this software were developed by Quenda IvS, 8 | Quenda (http://www.quenda.co/) 9 | -------------------------------------------------------------------------------- /configs/ponymail_nginx.conf: -------------------------------------------------------------------------------- 1 | # First, add the following to the global config: 2 | lua_package_path '/path/to/ponymail/site/api/?.lua;;'; 3 | 4 | # Then add this to your server (virtualhost) config: 5 | root /path/to/ponymail/site; 6 | location ~ ^/api/([-_a-zA-Z0-9]+)\.lua { 7 | set $path $1; 8 | content_by_lua_file /var/www/ponymail/site/api/$path.lua; 9 | } 10 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # The following modules are required. 2 | # It is believed that they have licences as shown 3 | # N.B. modules with license that are compatible with AL 2.0 should be included here 4 | # As such, html2text (GPL) and chardet (GPL) cannot be included 5 | certifi # MPL 2.0 6 | elasticsearch < 6.0.0 # AL2.0 7 | formatflowed # Python Software Foundation 8 | netaddr # BSD License, MIT License 9 | -------------------------------------------------------------------------------- /dockerfiles/ponymail_httpd_docker.conf: -------------------------------------------------------------------------------- 1 | 2 | LuaPackageCPath /usr/lib/lua/5.3/?.so 3 | LuaPackagePath /usr/share/lua/5.3/?.lua 4 | ServerName ponymail.localhost 5 | DocumentRoot /var/www/ponymail/site 6 | AddHandler lua-script .lua 7 | LuaScope thread 8 | LuaCodeCache stat 9 | AcceptPathInfo On 10 | AddOutputFilterByType DEFLATE application/json 11 | -------------------------------------------------------------------------------- /DISCLAIMER: -------------------------------------------------------------------------------- 1 | Apache Pony Mail is an effort undergoing incubation at The Apache Software Foundation (ASF), 2 | sponsored by the Apache Incubator. Incubation is required of all newly accepted projects until 3 | a further review indicates that the infrastructure, communications, and decision making process 4 | have stabilized in a manner consistent with other successful ASF projects. While incubation 5 | status is not necessarily a reflection of the completeness or stability of the code, it does 6 | indicate that the project has yet to be fully endorsed by the ASF. Apache Pony Mail is distributed 7 | under the Apache License v2.0. 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IDE files 2 | .idea 3 | *.iml 4 | .buildpath 5 | .project 6 | .pydevproject 7 | .settings/** 8 | 9 | # compiled output files 10 | __pycache__ 11 | 12 | # configuration files generated by setup.py 13 | site/api/lib/config.lua 14 | site/js/config.js 15 | tools/ponymail.cfg 16 | site/api/lib/config.lua.tmp 17 | tools/ponymail.cfg.tmp 18 | 19 | # site-local AAA module 20 | site/api/lib/aaa_site.lua 21 | 22 | 23 | # Note: 24 | # Add local overrides to one of the following files: 25 | # git config --get core.excludesfile - applies to all local git repos 26 | # $GIT_DIR/info/exclude - applies to the local git repo (not replicated) 27 | -------------------------------------------------------------------------------- /configs/ponymail_httpd.conf: -------------------------------------------------------------------------------- 1 | # Uncomment this if you have't loaded mod_lua yet: 2 | #LoadModule lua_module modules/mod_lua.so 3 | 4 | # Minimum requirements here: 5 | AddHandler lua-script .lua 6 | LuaScope thread 7 | # You could also use (for speedups/memory saving): 8 | #LuaScope server 5 25 9 | LuaCodeCache stat 10 | AcceptPathInfo On 11 | 12 | # Optionally enable compression of JSON objects: 13 | #AddOutputFilterByType DEFLATE application/json 14 | 15 | # For CentOS/RHEL with lua 5.3, you'll need to uncomment 16 | # the following directives: 17 | #LuaPackageCPath /usr/lib/lua/5.3/?.so 18 | #LuaPackagePath /usr/share/lua/5.3/?.lua 19 | -------------------------------------------------------------------------------- /test/mock_r.lua: -------------------------------------------------------------------------------- 1 | -- Mock version of various items for testing 2 | 3 | local _M = {} 4 | 5 | local r = { 6 | puts = function(r, ...) print(...) end, 7 | getcookie = function(r, name) return nil end, 8 | strcmp_match = function(str, pat) 9 | pat = pat:gsub("%.", "%%."):gsub("*", ".+") 10 | return str:match(pat) 11 | end, 12 | ivm_set = function(r, key, val) _M['ivm_' .. key] = val end, 13 | ivm_get = function(r, key) return _M['ivm_' .. key] end, 14 | } 15 | 16 | local function account(uid) 17 | return { 18 | credentials = { 19 | uid = uid, 20 | }, 21 | internal = { 22 | oauth_used = 'localhost', 23 | }, 24 | } 25 | end 26 | 27 | return { 28 | r = r, 29 | account = account 30 | } 31 | -------------------------------------------------------------------------------- /test/import_test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Test file importing 4 | 5 | echo ++++++++++++ Testing imports that should fail ++++++++++++ 6 | ../tools/import-mbox.py --dry --duplicates --source resources/fail 7 | echo ------------ Should be zero records inserted -------------- 8 | echo 9 | 10 | COUNT=$(grep '^From ' resources/pass/*.mbox | wc -l) 11 | echo ++++++++++++ Testing imports that should pass ++++++++++++ 12 | ../tools/import-mbox.py --dry --duplicates --source resources/pass 13 | echo ------------ Expecting $COUNT records inserted/updated and 0 bad records -------------- 14 | echo 15 | 16 | COUNT=$(grep '^From ' resources/valid/*.mbox | wc -l) 17 | echo "++++++++++++ Testing imports that should pass but don't currently ++++++++++++" 18 | ../tools/import-mbox.py --dry --duplicates --source resources/valid 19 | echo ------------ Expecting 0 records inserted/updated and $COUNT bad records -------------- 20 | echo 21 | -------------------------------------------------------------------------------- /STATUS: -------------------------------------------------------------------------------- 1 | Pony Mail STATUS: 2 | Last modified at [19 Sept 2016] 3 | 4 | The current version of this file can be found at: 5 | 6 | * https://gitbox.apache.org/repos/asf?p=incubator-ponymail.git;a=blob_plain;f=STATUS 7 | 8 | The purpose of this file is to track proposed changes to master. Each 9 | proposed change requires a +1 from two people, one of which may be from 10 | the original author, provided the author is a committer on the pony mail 11 | project. Once sufficient +1s have been reached, either party may merge 12 | the changes to master. 13 | 14 | PATCHES/ISSUES THAT ARE PROPOSED FOR MASTER: 15 | 16 | * Make sure we don't archive emails with NULL-bodies. 17 | commit: 81bd75a42ce8fa3387c3bc371785de96f2ee047b 18 | shortlog: https://git-wip-us.apache.org/repos/asf?p=incubator-ponymail.git;a=shortlog;h=refs/heads/null-bodies 19 | +1: Humbedooh, ucb 20 | 21 | -------- 22 | Example: 23 | * Fix all the Ponies 24 | commit: bd27ae9a5099b1591d3d2c27a2c2c631350553ca 25 | shortlog: https://git-wip-us.apache.org/repos/asf?p=incubator-ponymail.git;a=shortlog;h=refs/heads/fix-ponies 26 | +1: Humbedooh, ucb 27 | 28 | 29 | -------------------------------------------------------------------------------- /dockerfiles/debian/Dockerfile: -------------------------------------------------------------------------------- 1 | ############################################################ 2 | # Dockerfile to build Pony Mail container images 3 | # Based on Debian 4 | ############################################################ 5 | 6 | # Set base images 7 | FROM debian 8 | FROM elasticsearch 9 | 10 | MAINTAINER Daniel Gruno 11 | 12 | # Update aptitude repo data 13 | RUN apt-get update 14 | 15 | # Install base packages 16 | RUN apt-get install -y apache2 git lua-cjson lua-sec lua-socket python3 python3-pip 17 | RUN pip3 install elasticsearch formatflowed netaddr 18 | 19 | 20 | # Download Pony Mail 21 | RUN git clone https://github.com/apache/incubator-ponymail.git /var/www/ponymail 22 | 23 | # Add httpd config 24 | ADD https://raw.githubusercontent.com/apache/incubator-ponymail/master/dockerfiles/ponymail_httpd_docker.conf /etc/apache2/sites-enabled/000-default.conf 25 | 26 | 27 | # Start ElasticSearch, set up Pony Mail 28 | EXPOSE 9200 9300 29 | RUN service elasticsearch start && sleep 30 && service elasticsearch status && cd /var/www/ponymail/tools && python3 setup.py --defaults 30 | 31 | # Enable mod_lua 32 | RUN a2enmod lua 33 | 34 | # Expose port for httpd 35 | EXPOSE 80 36 | 37 | # Set default container startup sequence 38 | ENTRYPOINT service elasticsearch start && service apache2 start && bash 39 | -------------------------------------------------------------------------------- /aaa_examples/README.md: -------------------------------------------------------------------------------- 1 | # AAA Examples 2 | This directory contains example AAA (Authentication, Authorization and Access) 3 | libraries for various use cases. 4 | 5 | To activate one of these scripts (or derivatives thereof), simply copy the appropriate 6 | AAA script to `site/api/lib/aaa_site.lua`. 7 | This will then be used by the main aaa.lua script. 8 | 9 | These scripts require that 10 | `site/api/lib/config.lua` has one or more OAuth providers specified as 11 | authorities, as such: 12 | 13 | ~~~ 14 | ..., 15 | -- Use Google OAuth as an authority 16 | admin_oauth = { "www.googleapis.com" } 17 | ... 18 | ~~~ 19 | 20 | ### AAA by email address: 21 | [`aaa_by_email_address.lua`](aaa_by_email_address.lua) checks against a GLOB 22 | (`valid_email`), and if a logged-in user's email address matches this, provides 23 | access to private lists, provided the OAuth provider used is listed in 24 | `config.lua` as a valid authority. 25 | 26 | 27 | ### AAA by OAuth portal: 28 | [`aaa_by_portal.lua`](aaa_by_portal.lua) checks which OAuth portal was used to 29 | log in. If it's the right (Google in the example), then access to private lists 30 | is granted. 31 | 32 | 33 | ### AAA with access list: 34 | [`aaa_with_subgroups.lua`](aaa_with_subgroups.lua) checks validated accounts 35 | against an access list, and if found, provides access to a specific set of 36 | lists for each individual user. 37 | -------------------------------------------------------------------------------- /site/js/dev/combine.sh: -------------------------------------------------------------------------------- 1 | echo "Combining JS..." 2 | echo '/* 3 | Licensed to the Apache Software Foundation (ASF) under one or more 4 | contributor license agreements. See the NOTICE file distributed with 5 | this work for additional information regarding copyright ownership. 6 | The ASF licenses this file to You under the Apache License, Version 2.0 7 | (the "License"); you may not use this file except in compliance with 8 | the License. You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | */ 18 | // THIS IS AN AUTOMATICALLY COMBINED FILE. PLEASE EDIT dev/*.js!! 19 | ' > ../ponymail.js 20 | # Warning: ls/sort order depends on the locale; this can affect the order 21 | # of non-alphanumerics such as '.' and '_'. So force the use of 'C' locale 22 | for f in `LC_ALL=C ls *.js`; do 23 | printf "\n\n/******************************************\n Fetched from dev/${f}\n******************************************/\n\n" >> ../ponymail.js 24 | sed -e '/^\/\*/,/\*\//d' ${f} >> ../ponymail.js 25 | done 26 | echo "Done!" 27 | -------------------------------------------------------------------------------- /tools/ponymailconfig.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # Licensed to the Apache Software Foundation (ASF) under one or more 4 | # contributor license agreements. See the NOTICE file distributed with 5 | # this work for additional information regarding copyright ownership. 6 | # The ASF licenses this file to You under the Apache License, Version 2.0 7 | # (the "License"); you may not use this file except in compliance with 8 | # the License. You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | """ 19 | common config parsing 20 | reads the file ponymail.cfg from the same directory as this file 21 | 22 | How to use: 23 | 24 | from ponymailconfig import PonymailConfig 25 | config=PonymailConfig() 26 | if config.has_option('elasticsearch', 'user'): 27 | ... 28 | """ 29 | 30 | import os.path 31 | from configparser import RawConfigParser 32 | 33 | class PonymailConfig(RawConfigParser): 34 | 35 | def __init__(self,*args, **kwargs): 36 | super().__init__(*args, **kwargs) 37 | path = os.path.dirname(os.path.realpath(__file__)) 38 | RawConfigParser.read(self, "%s/ponymail.cfg" % path) 39 | -------------------------------------------------------------------------------- /tools/json_tidy.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Licensed to the Apache Software Foundation (ASF) under one or more 4 | # contributor license agreements. See the NOTICE file distributed with 5 | # this work for additional information regarding copyright ownership. 6 | # The ASF licenses this file to You under the Apache License, Version 2.0 7 | # (the "License"); you may not use this file except in compliance with 8 | # the License. You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | """ 19 | Prettify json: indent, sort 20 | Can also drop keys that aren't wanted, e.g. the debug array, to make diffs easier 21 | """ 22 | 23 | import sys 24 | import json 25 | import argparse 26 | 27 | parser = argparse.ArgumentParser() 28 | parser.add_argument("--indent", type=int, help="Indentation to use for the output file (default 1)", default=1) 29 | parser.add_argument("--drop", help="Comma-separated list of top-level keys to drop (e.g. debug,took)", default='') 30 | 31 | args = parser.parse_args() 32 | 33 | inp = json.loads(sys.stdin.read()) 34 | for key in args.drop.split(','): 35 | try: 36 | del inp[key] 37 | except KeyError: 38 | pass 39 | 40 | json.dump(inp, sys.stdout, indent=args.indent, sort_keys=True) 41 | print("") # EOL at EOF 42 | -------------------------------------------------------------------------------- /.github/workflows/pythonpackage.yml: -------------------------------------------------------------------------------- 1 | name: Python package 2 | 3 | on: [push,workflow_dispatch] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | strategy: 10 | max-parallel: 4 11 | matrix: 12 | # python-version: [2.7, 3.5, 3.6, 3.7] 13 | python-version: [3.7] 14 | 15 | steps: 16 | - uses: actions/checkout@master 17 | with: 18 | persist-credentials: false 19 | - uses: actions/checkout@master 20 | with: 21 | persist-credentials: false 22 | repository: apache/incubator-ponymail-unit-tests 23 | path: pmtests 24 | - name: Set up Python ${{ matrix.python-version }} 25 | uses: actions/setup-python@v1 26 | with: 27 | python-version: ${{ matrix.python-version }} 28 | - name: Install dependencies 29 | run: | 30 | python -m pip install --upgrade pip 31 | pip install -r test/requirements.txt 32 | - name: Set up LUA 33 | # This is the commit for v8.0.0 (current at time of approval by INFRA) 34 | uses: leafo/gh-actions-lua@ea0ae38722c0b45aa4e770f7c4a650c6b26800d0 35 | with: 36 | luaVersion: "5.2" 37 | - name: Basic Test with LUA 38 | run: lua -v 39 | # - name: Test with pytest 40 | # run: | 41 | # pip install pytest 42 | # pytest 43 | - name: Test with Ponymail Unit tests 44 | run: | 45 | sed -e 's/# cropout:/cropout:/' tools/ponymail.cfg.sample >tools/ponymail.cfg 46 | cd pmtests 47 | python runall.py --root .. 48 | - name: Generator tests 49 | run: | 50 | cd test 51 | python generatortest.py generatortest.yaml 52 | -------------------------------------------------------------------------------- /tools/update-asf-lists.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ############################################################################ 3 | # Licensed to the Apache Software Foundation (ASF) under one or more # 4 | # contributor license agreements. See the NOTICE file distributed with # 5 | # this work for additional information regarding copyright ownership. # 6 | # The ASF licenses this file to you under the Apache License, Version 2.0 # 7 | # (the "License"); you may not use this file except in compliance with # 8 | # the License. You may obtain a copy of the License at # 9 | # # 10 | # http://www.apache.org/licenses/LICENSE-2.0 # 11 | # # 12 | # Unless required by applicable law or agreed to in writing, software # 13 | # distributed under the License is distributed on an "AS IS" BASIS, # 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # 15 | # See the License for the specific language governing permissions and # 16 | # limitations under the License. # 17 | ############################################################################ 18 | DIR=$1 19 | for tlp in `ls -1 ${DIR}`; do 20 | if [ "${tlp}" != "DONE" ]; then 21 | TLPN=`echo ${tlp} | cut -d. -f1` 22 | if [ "${TLPN}" != "incubator" ]; then 23 | printf "\n*** Updating ${TLPN} ***\n" 24 | python3 import-mbox.py --source "http://mail-archives.eu.apache.org/mod_mbox/" --quick --mod-mbox --project ${TLPN}; 25 | fi 26 | fi 27 | done 28 | 29 | -------------------------------------------------------------------------------- /tools/ponymail.cfg.sample: -------------------------------------------------------------------------------- 1 | ############################################################### 2 | # A ponymail.cfg is needed to run this project. This sample config file was 3 | # originally generated by tools/setup.py. 4 | # 5 | # Run the tools/setup.py script and a ponymail.cfg which looks a lot like this 6 | # one will be generated. If, for whatever reason, that script is not working 7 | # for you, you may use this ponymail.cfg as a starting point. 8 | # 9 | # Contributors should strive to keep this sample updated. One way to do this 10 | # would be to run the tools/setup.py, rename the generated config to 11 | # ponymail.cfg.sample, and then pasting this message or a modified form of 12 | # this message at the top. 13 | ############################################################### 14 | 15 | ############################################################### 16 | # Pony Mail Configuration file 17 | 18 | # Main ES configuration 19 | [elasticsearch] 20 | hostname: localhost 21 | dbname: ponymail 22 | port: 9200 23 | ssl: false 24 | 25 | #uri: url_prefix 26 | 27 | #user: username 28 | #password: password 29 | 30 | #wait: active shard count 31 | 32 | #backup: database name 33 | 34 | [archiver] 35 | #generator: medium|full|cluster|other 36 | #baseurl: https://my.archive.example.com 37 | 38 | [debug] 39 | #cropout: string to crop from list-id 40 | # e.g. Strip out incubator except at top level 41 | # cropout: (\w+\.\w+)\.incubator\.apache\.org \1.apache.org 42 | 43 | ############################################################### 44 | -------------------------------------------------------------------------------- /tools/import-all-asf-lists.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ############################################################################ 3 | # Licensed to the Apache Software Foundation (ASF) under one or more # 4 | # contributor license agreements. See the NOTICE file distributed with # 5 | # this work for additional information regarding copyright ownership. # 6 | # The ASF licenses this file to you under the Apache License, Version 2.0 # 7 | # (the "License"); you may not use this file except in compliance with # 8 | # the License. You may obtain a copy of the License at # 9 | # # 10 | # http://www.apache.org/licenses/LICENSE-2.0 # 11 | # # 12 | # Unless required by applicable law or agreed to in writing, software # 13 | # distributed under the License is distributed on an "AS IS" BASIS, # 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # 15 | # See the License for the specific language governing permissions and # 16 | # limitations under the License. # 17 | ############################################################################ 18 | DIR=$1 19 | for tlp in `ls -1 ${DIR}`; do 20 | for list in `ls -1 ${DIR}/${tlp}/ | sort -g`; do 21 | if [ "${tlp}" != "DONE" ]; then 22 | TLPN=`echo ${tlp} | cut -d. -f1` 23 | if [ "${TLPN}" != "${list}" ]; then 24 | printf "\n*** Importing ${tlp}/${list} ***\n" 25 | python3 ./import-mbox.py --source ${DIR}/${tlp}/${list} --ext "" --lid "<${list}.${tlp}>" --attachments; 26 | fi 27 | fi 28 | done 29 | done 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /site/api/websearch.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Licensed to the Apache Software Foundation (ASF) under one or more 3 | contributor license agreements. See the NOTICE file distributed with 4 | this work for additional information regarding copyright ownership. 5 | The ASF licenses this file to You under the Apache License, Version 2.0 6 | (the "License"); you may not use this file except in compliance with 7 | the License. You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | ]]-- 17 | 18 | -- This is websearch.lua - a script for adding OpenSearch engines 19 | local cross = require 'lib/cross' 20 | 21 | function handle(r) 22 | local domain = r:escape_html(r.args) 23 | local scheme = "https" 24 | if r.port == 80 then 25 | scheme = "http" 26 | end 27 | local hostname = ("%s://%s:%u"):format(scheme, r.hostname, r.port) 28 | cross.contentType(r, 'application/opensearchdescription+xml') 29 | r:puts(([[ 30 | 31 | Pony Mail: %s 32 | Search for emails on the %s mailing lists 33 | mailing lists, email 34 | %s/favicon.ico 35 | 36 | 37 | 38 | ]]):format(domain, domain, hostname, hostname, domain)) 39 | return cross.OK 40 | end 41 | 42 | cross.start(handle) -------------------------------------------------------------------------------- /site/api/lib/trace.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Licensed to the Apache Software Foundation (ASF) under one or more 3 | contributor license agreements. See the NOTICE file distributed with 4 | this work for additional information regarding copyright ownership. 5 | The ASF licenses this file to You under the Apache License, Version 2.0 6 | (the "License"); you may not use this file except in compliance with 7 | the License. You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | ]]-- 17 | 18 | --[[ 19 | Simple trace facility for local debugging. 20 | Shows a message together with its location. 21 | Output is to stderr as that should appear in the server logs 22 | 23 | N.B. This is intended as a handy tool for local use only; 24 | not intended for use in deployed code. 25 | 26 | Usage: 27 | 28 | require 'lib/trace' 29 | 30 | ... 31 | 32 | whereami() 33 | 34 | ... 35 | 36 | trace("Status: " .. status) 37 | 38 | ]]-- 39 | 40 | -- show calling location details 41 | function _whereami(depth) 42 | depth = depth or 0 43 | local data = debug.getinfo(2+depth,"Snl") 44 | return data.short_src:gsub('^.+/','') .. "#" .. (data.name or '') .. "@" .. data.currentline .. ": " 45 | end 46 | 47 | function trace(s, depth) 48 | depth = depth or 0 49 | -- Use a leading marker to make it easier to find in the logs 50 | io.stderr:write("@(#) " .. _whereami(1+depth) .. tostring(s) .. "\n") 51 | end 52 | 53 | --[[ 54 | debug.getinfo output 55 | { "S" 56 | lastlinedefined = 0, 57 | linedefined = 0, 58 | short_src = "../test/stack.lua", 59 | source = "@../test/stack.lua", 60 | what = "main" 61 | } 62 | { "n" 63 | name = "one", 64 | namewhat = "global" 65 | } 66 | { "l" 67 | currentline = 12 68 | } 69 | ]]-- 70 | -------------------------------------------------------------------------------- /site/js/dev/ponymail_favorites.js: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one or more 3 | contributor license agreements. See the NOTICE file distributed with 4 | this work for additional information regarding copyright ownership. 5 | The ASF licenses this file to You under the Apache License, Version 2.0 6 | (the "License"); you may not use this file except in compliance with 7 | the License. You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | 18 | // Callback func for favorite/forget 19 | // this just alerts and reverses the fav button 20 | function favCallback(json, state) { 21 | var fvb = document.getElementById('favbtn') 22 | if (state[0]) { 23 | alert(state[1] + " added to favorites!") 24 | // fav button? set it to a 'remove' button 25 | if (fvb) { 26 | fvb.innerHTML = '   Remove from favorites' 27 | } 28 | } else { 29 | alert(state[1] + " removed from favorites!") 30 | // remove button exists? set it to a 'fav this' button 31 | if (fvb) { 32 | fvb.innerHTML = '   Add list to favorites' 33 | } 34 | } 35 | } 36 | 37 | // Favorite/forget call: either sub or unsub a list from favorites 38 | function favorite(sub, list) { 39 | // favorite? 40 | if (sub) { 41 | GetAsync("/api/preferences.lua?addfav="+list, [sub,list], favCallback) 42 | } 43 | // forget? 44 | else { 45 | GetAsync("/api/preferences.lua?remfav="+list, [sub,list], favCallback) 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /aaa_examples/aaa_by_portal.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Licensed to the Apache Software Foundation (ASF) under one or more 3 | contributor license agreements. See the NOTICE file distributed with 4 | this work for additional information regarding copyright ownership. 5 | The ASF licenses this file to You under the Apache License, Version 2.0 6 | (the "License"); you may not use this file except in compliance with 7 | the License. You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | ]]-- 17 | 18 | -- This is aaa.lua - AAA filter with portal validation. 19 | -- It checks that an oauth validated account was logged in through a specific 20 | -- OAuth provider, and if used, allows access to private lists. 21 | -- To use this as your AAA lib, replace aaa.lua in site/api/lib with this file. 22 | 23 | local config = require 'lib/config' 24 | 25 | -- Allow anyone logged in through Google+ access to private emails 26 | -- This is a direct string match, not a GLOB 27 | local valid_portal = "www.googleapis.com" 28 | local grant_access_to = "*" -- use * for access to all, or specify a (sub)domain to grant access to 29 | 30 | -- Get rights (full or no access) 31 | local function getRights(r, usr) 32 | local email = usr.credentials.email or "|||" 33 | local xemail = email:match("([-a-zA-Z0-9._@]+)") -- whitelist characters 34 | local rights = {} 35 | 36 | -- bad char in email? 37 | if not email or xemail ~= email then 38 | return rights 39 | end 40 | 41 | -- Check if admin or if the right oauth portal was used 42 | if usr.internal.admin or oauth_domain == valid_portal then 43 | table.insert(rights, grant_access_to) 44 | end 45 | return rights 46 | end 47 | 48 | -- module defs 49 | return { 50 | validateParams = true, 51 | rights = getRights 52 | } 53 | -------------------------------------------------------------------------------- /tools/trace.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # Licensed to the Apache Software Foundation (ASF) under one or more 4 | # contributor license agreements. See the NOTICE file distributed with 5 | # this work for additional information regarding copyright ownership. 6 | # The ASF licenses this file to You under the Apache License, Version 2.0 7 | # (the "License"); you may not use this file except in compliance with 8 | # the License. You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | """ 19 | Simple trace facility for local debugging. 20 | Shows a message together with its location. 21 | 22 | N.B. This is intended as a handy tool for local use only; 23 | not intended for use in deployed code. 24 | 25 | Usage: 26 | 27 | from trace import trace 28 | ... 29 | trace("Message-id %s" % message-id) 30 | ... 31 | trace("Message-id %s" % message-id) 32 | ... 33 | trace("Message-id %s" % message-id) 34 | 35 | The output includes the line number so there is 36 | no need to customise the messages to id them. 37 | """ 38 | import inspect 39 | from os.path import basename 40 | 41 | def trace(s='', depth=1): 42 | """ 43 | Show message with context from the caller 44 | """ 45 | stack=inspect.stack() 46 | maxIndex = len(stack) - 1 47 | depth = maxIndex if depth >= maxIndex else depth 48 | _frame,filename,line_number,function_name,_lines,_index = stack[depth] 49 | print(">>>>[%d]%s@%s#%s: %s"%(depth,basename(filename),line_number,function_name,s)) 50 | 51 | def func_name(depth=1): 52 | """ 53 | Return the caller's name 54 | """ 55 | stack=inspect.stack() 56 | maxIndex = len(stack) - 1 57 | depth = maxIndex if depth >= maxIndex else depth 58 | _frame,_filename,_linenumber,function_name,_lines,_index = stack[depth] 59 | return function_name 60 | 61 | if __name__ == '__main__': 62 | trace("test") 63 | trace("test",0) 64 | trace("test",2) 65 | -------------------------------------------------------------------------------- /test/resources/pass/emptyattach2.mbox: -------------------------------------------------------------------------------- 1 | From tomcat-user-return-50118-qmlist-jakarta-archive-tomcat-user=jakarta.apache.org@jakarta.apache.org Fri Jan 24 12:24:14 2003 2 | Return-Path: 3 | Delivered-To: apmail-jakarta-tomcat-user-archive@apache.org 4 | Received: (qmail 13295 invoked from network); 24 Jan 2003 12:24:14 -0000 5 | Received: from exchange.sun.com (192.18.33.10) 6 | by 208.185.179.12.available.above.net with SMTP; 24 Jan 2003 12:24:14 -0000 7 | Received: (qmail 24215 invoked by uid 97); 24 Jan 2003 12:25:16 -0000 8 | Delivered-To: qmlist-jakarta-archive-tomcat-user@jakarta.apache.org 9 | Received: (qmail 24199 invoked by uid 97); 24 Jan 2003 12:25:16 -0000 10 | Mailing-List: contact tomcat-user-help@jakarta.apache.org; run by ezmlm 11 | Precedence: bulk 12 | List-Unsubscribe: 13 | List-Subscribe: 14 | List-Help: 15 | List-Post: 16 | List-Id: "Tomcat Users List" 17 | Reply-To: "Tomcat Users List" 18 | Delivered-To: mailing list tomcat-user@jakarta.apache.org 19 | Received: (qmail 24187 invoked by uid 98); 24 Jan 2003 12:25:15 -0000 20 | X-Antivirus: nagoya (v4218 created Aug 14 2002) 21 | Message-ID: <004201c2c3a3$82f86e40$160aa8c0@win98> 22 | From: "Henry" 23 | To: "Tomcat Users List" 24 | Subject: how do I detect alive sessions at this moment? 25 | Date: Fri, 24 Jan 2003 20:24:15 +0800 26 | MIME-Version: 1.0 27 | Content-Type: multipart/alternative; 28 | boundary="----=_NextPart_000_003F_01C2C3E6.90B6F900" 29 | X-Priority: 3 30 | X-MSMail-Priority: Normal 31 | X-Mailer: Microsoft Outlook Express 6.00.2800.1106 32 | X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2800.1106 33 | X-Spam-Rating: 208.185.179.12.available.above.net 1.6.2 0/1000/N 34 | X-Spam-Rating: 208.185.179.12.available.above.net 1.6.2 0/1000/N 35 | 36 | ------=_NextPart_000_003F_01C2C3E6.90B6F900 37 | Content-Type: text/plain; 38 | charset="big5" 39 | Content-Transfer-Encoding: quoted-printable 40 | 41 | 42 | ------=_NextPart_000_003F_01C2C3E6.90B6F900-- 43 | 44 | 45 | -------------------------------------------------------------------------------- /site/api/lib/config.lua.sample: -------------------------------------------------------------------------------- 1 | --[[ 2 | A config.lua is needed to run this project. This sample config file was 3 | originally generated by tools/setup.py. 4 | 5 | Run the tools/setup.py script and a config.lua which looks a lot like this one will 6 | be generated. If, for whatever reason, that script is not working for you, you 7 | may use this config.lua as a starting point. 8 | 9 | Contributors should strive to keep this sample updated. One way to do this would 10 | be to run the tools/setup.py, rename the generated config to config.lua.sample, 11 | and then pasting this message or a modified form of this message at the top. 12 | ]] 13 | 14 | local config = { 15 | es_url = "http://localhost:9200/ponymail2/", 16 | mailserver = "mail.foo.org", 17 | -- mailport = 1025, -- override the default port (25) 18 | accepted_domains = "*", 19 | wordcloud = false, 20 | email_footer = nil, -- see the docs for how to set this up. 21 | full_headers = false, 22 | maxResults = 5000, -- max emails to return in one go. Might need to be bumped for large lists 23 | -- stats_maxBody = 200, -- max size of body snippet returned by stats.lua 24 | -- stats_wordExclude = ".|..|...", -- patterns to exclude from word cloud generated by stats.lua 25 | admin_oauth = {}, -- list of domains that may do administrative oauth (private list access) 26 | -- add 'www.googleapis.com' to the list for google oauth to decide, for instance. 27 | oauth_fields = { -- used for specifying individual oauth handling parameters. 28 | -- for example: 29 | -- internal = { 30 | -- email = 'CAS-EMAIL', 31 | -- name = 'CAS-NAME', 32 | -- uid = 'REMOTE-USER', 33 | -- env = 'subprocess' -- use environment vars instead of request headers 34 | -- } 35 | }, 36 | -- allow_insecure_cookie = true, -- override the default (false) - only use for test installations 37 | -- no_association = {}, -- domains that are not allowed for email association 38 | -- listsDisplay = 'regex', -- if defined, hide list names that don't match the regex 39 | -- debug = false, -- whether to return debug information 40 | antispam = true, -- Whether or not to add anti-spam measures aimed at anonymous users. 41 | noShowQuery = false, -- disallow return query in JSON result: true|false (default false) 42 | } 43 | return config 44 | -------------------------------------------------------------------------------- /site/js/dev/ponymail_seeders.js: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one or more 3 | contributor license agreements. See the NOTICE file distributed with 4 | this work for additional information regarding copyright ownership. 5 | The ASF licenses this file to You under the Apache License, Version 2.0 6 | (the "License"); you may not use this file except in compliance with 7 | the License. You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | 18 | // seedGetListInfo: Callback that seeds the list index and sets up account stuff 19 | function seedGetListInfo(json, state) { 20 | all_lists = json.lists 21 | if (typeof json.preferences != undefined && json.preferences) { 22 | prefs = json.preferences 23 | } 24 | // did the backend supply us with a valid login? 25 | // if so, set up the menu bar and save locally 26 | if (typeof json.login != undefined && json.login) { 27 | login = json.login 28 | if (login.credentials) { 29 | setupUser(login) 30 | } 31 | } 32 | 33 | // Actual callback: render list 34 | getListInfo(state.l, state.x, state.n) 35 | } 36 | 37 | // seedPrefs: get prefs/login and call something else 38 | function seedPrefs(json, state) { 39 | if (typeof json.preferences != undefined && json.preferences) { 40 | prefs = json.preferences 41 | } 42 | // logged in? render user nav bar then 43 | if (typeof json.login != undefined && json.login) { 44 | login = json.login 45 | if (login.credentials) { 46 | setupUser(login) 47 | } 48 | } 49 | // Do we have a callback waiting? if so, run it 50 | if (state && state.docall) { 51 | GetAsync(state.docall[0], null, state.docall[1]) 52 | } 53 | } 54 | // preGetListInfo: Callback that fetches preferences and sets up list data 55 | // invoked by onload in list.html and search.html 56 | function preGetListInfo(list, xdomain, nopush) { 57 | GetAsync("/api/preferences.lua", { 58 | l: list, 59 | x: xdomain, 60 | n: nopush 61 | }, seedGetListInfo) 62 | } 63 | 64 | -------------------------------------------------------------------------------- /test/luasocket_test.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Licensed to the Apache Software Foundation (ASF) under one or more 3 | contributor license agreements. See the NOTICE file distributed with 4 | this work for additional information regarding copyright ownership. 5 | The ASF licenses this file to You under the Apache License, Version 2.0 6 | (the "License"); you may not use this file except in compliance with 7 | the License. You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | ]]-- 17 | 18 | -- Test script for http.socket showing different response types 19 | 20 | local http = require 'socket.http' 21 | 22 | --[[ 23 | The luasock library is documented at: 24 | http://w3.impa.br/~diego/software/luasocket/http.html#request 25 | This gives the following information on the returned values: 26 | 27 | #: On Success On Failure 28 | 1: body (string) nil 29 | 2: code (number) message (string) 30 | 3: headers (table) nil 31 | 4: status line nil 32 | 33 | ]]-- 34 | 35 | function runRequest(url, query) 36 | print("=========", url, query) 37 | local response = {http.request(url, query)} -- pick up all the response 38 | print("#values: ",#response) 39 | for i = 1,#response do 40 | local v = response[i] 41 | if i == 2 or i == 4 then 42 | print(i,type(v),v) 43 | else 44 | print(i,type(v),#(v or "")) 45 | if i == 3 and type(v) == "table" then 46 | for k,v in pairs(v) do 47 | print("",k,v) 48 | end 49 | end 50 | if i == 1 and v then 51 | print(v:sub(1,math.min(132,#v))) 52 | end 53 | end 54 | end 55 | local hc = response[2] 56 | if hc ~= 200 then -- show the error message 57 | print(response[1]) 58 | end 59 | end 60 | 61 | 62 | if #arg >0 then 63 | runRequest(unpack(arg)) 64 | else 65 | runRequest("http://localhost:9200/_cat/indices") -- valid 66 | runRequest("http://localhost:9200/_dog/indices") -- invalid 67 | runRequest("http://localhost:92000/_cat/indices") -- port invalid 68 | end 69 | -------------------------------------------------------------------------------- /site/js/dev/ponymail_stats.js: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one or more 3 | contributor license agreements. See the NOTICE file distributed with 4 | this work for additional information regarding copyright ownership. 5 | The ASF licenses this file to You under the Apache License, Version 2.0 6 | (the "License"); you may not use this file except in compliance with 7 | the License. You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | 18 | 19 | // showStats: Show the ML stats on the front page 20 | function showStats(json) { 21 | var obj = document.getElementById('list_stats') 22 | 23 | // top bar stats 24 | obj.innerHTML = "

Overall 14 day activity:

" 25 | obj.innerHTML += ' ' + json.participants.toLocaleString() + " People   " 26 | obj.innerHTML += ' ' + json.hits.toLocaleString() + ' messages  '; 27 | obj.innerHTML += ' ' + json.no_threads.toLocaleString() + " topics   " 28 | obj.innerHTML += ' ' + json.no_active_lists.toLocaleString() + " active lists." 29 | 30 | 31 | // Make a table (cheap way to graph stuff) for the daily stats 32 | var ts = "" 33 | 34 | // find the max no. of emails in a single day, for calculating max height of the 14 day chart 35 | var max = 1 36 | for (var i in json.activity) { 37 | max = Math.max(max, json.activity[i][1]) 38 | } 39 | 40 | // for each day, make a bar, taking into account the max value 41 | for (var i in json.activity) { 42 | var day = new Date(json.activity[i][0]).toDateString() 43 | ts += "" 44 | } 45 | ts += "
" 46 | obj.innerHTML += ts 47 | } 48 | -------------------------------------------------------------------------------- /site/api/source.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Licensed to the Apache Software Foundation (ASF) under one or more 3 | contributor license agreements. See the NOTICE file distributed with 4 | this work for additional information regarding copyright ownership. 5 | The ASF licenses this file to You under the Apache License, Version 2.0 6 | (the "License"); you may not use this file except in compliance with 7 | the License. You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | ]]-- 17 | 18 | -- This is source.lua - a script for displaying the source of an email 19 | 20 | local elastic = require 'lib/elastic' 21 | local aaa = require 'lib/aaa' 22 | local user = require 'lib/user' 23 | local cross = require 'lib/cross' 24 | local utils = require 'lib/utils' 25 | 26 | function handle(r) 27 | -- content is currently utf-8, see #367 28 | cross.contentType(r, "text/plain; charset=utf-8") 29 | local get = r:parseargs() 30 | -- get the parameter (if any) and tidy it up 31 | local eid = (get.id or r.path_info):gsub("\"", ""):gsub("/", "") 32 | -- If it is the empty string then set it to "1" so ES doesn't barf 33 | -- N.B. ?id is treated as ?id=1 34 | if #eid == 0 then eid = "1" end 35 | local doc = elastic.get("mbox", eid, true) 36 | 37 | -- Try searching by mid if not found, for backward compat 38 | if not doc or not doc.mid then 39 | local docs = elastic.find("message-id:\"" .. r:escape(eid) .. "\"", 1, "mbox") 40 | if #docs == 1 then 41 | doc = docs[1] 42 | end 43 | end 44 | if doc and doc.mid then 45 | local account = user.get(r) 46 | if aaa.canAccessDoc(r, doc, account) then 47 | local doc_raw = elastic.get('mbox_source', doc.request_id) 48 | if doc_raw then 49 | r:write(doc_raw.source) 50 | else 51 | r:puts("Could not find the email source, sorry!") 52 | end 53 | return cross.OK 54 | -- N.B. no need to check for shortened links here as they are not used for the source 55 | end 56 | end 57 | r:puts[[No such email, or you don't have access. Sorry!]] 58 | return cross.OK 59 | end 60 | 61 | cross.start(handle) -------------------------------------------------------------------------------- /tools/nullfav.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # Licensed to the Apache Software Foundation (ASF) under one or more 4 | # contributor license agreements. See the NOTICE file distributed with 5 | # this work for additional information regarding copyright ownership. 6 | # The ASF licenses this file to You under the Apache License, Version 2.0 7 | # (the "License"); you may not use this file except in compliance with 8 | # the License. You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | """ 19 | 20 | Scan accounts to drop null entries from favorites 21 | 22 | """ 23 | 24 | import argparse 25 | from elastic import Elastic 26 | 27 | parser = argparse.ArgumentParser(description='Command line options.') 28 | 29 | parser.add_argument('--apply', dest='apply', action='store_true', 30 | help='Update the account favorites. Default is list ids only') 31 | 32 | args = parser.parse_args() 33 | 34 | FAVES='favorites' 35 | TARGET=None 36 | 37 | updated = 0 38 | failed = 0 39 | elastic = Elastic() 40 | scroll_size = None # Only show it first time round 41 | for page in elastic.scan_and_scroll(doc_type='account', body = { "_source" : [FAVES] }): 42 | if not scroll_size: 43 | scroll_size = page['hits']['total'] 44 | print("Found %d accounts" % scroll_size) 45 | for hit in page['hits']['hits']: 46 | mid = hit['_id'] 47 | source = hit['_source'] 48 | if FAVES in source: 49 | favorites = source[FAVES] 50 | if TARGET in favorites: 51 | newfav = [x for x in favorites if x != TARGET] 52 | if not args.apply: 53 | print("Would update account mid %s" % mid) 54 | continue 55 | print("Updating account mid %s" % mid) 56 | try: 57 | elastic.update(doc_type='account', 58 | id = mid, 59 | body = { 60 | 'doc': { 61 | FAVES: newfav 62 | } 63 | } 64 | ) 65 | updated +=1 66 | except Exception as e: 67 | print("Error updating mid %s: %s" % (mid,e)) 68 | failed += 1 69 | 70 | if args.apply: 71 | print("Updated %d account(s) with %d failures" % (updated, failed)) 72 | -------------------------------------------------------------------------------- /tools/email_utils_patch.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # Licensed to the Apache Software Foundation (ASF) under one or more 4 | # contributor license agreements. See the NOTICE file distributed with 5 | # this work for additional information regarding copyright ownership. 6 | # The ASF licenses this file to You under the Apache License, Version 2.0 7 | # (the "License"); you may not use this file except in compliance with 8 | # the License. You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | """ 19 | Possible patch for Python email package 20 | The current code calls unquote() in collapse_rfc2231_value 21 | when it has already called unquote(). 22 | Double-unquoting mangles raw values that happen to be enclosed in quotes. 23 | 24 | This was first discovered for multi-part boundaries, some of which look like: 25 | boundary="<<>>" 26 | Strictly speaking < and > are not valid as boundary chars, but they are seen in the wild. 27 | A similar problem exists for filenames which start/end with "/" or 28 | These are valid (but unusual) 29 | 30 | One way to fix this is to replace the faulty version of collapse_rfc2231_value. 31 | 32 | To use: 33 | 34 | import email_utils_patch 35 | ... 36 | email_utils_patch.patch() 37 | 38 | """ 39 | 40 | from email import utils 41 | 42 | # Copy of utils.collapse_rfc2231_value with unquote() calls removed 43 | def _collapse_rfc2231_value(value, errors='replace', 44 | fallback_charset='us-ascii'): 45 | if not isinstance(value, tuple) or len(value) != 3: 46 | return value 47 | # While value comes to us as a unicode string, we need it to be a bytes 48 | # object. We do not want bytes() normal utf-8 decoder, we want a straight 49 | # interpretation of the string as character bytes. 50 | charset, _language, text = value 51 | if charset is None: 52 | # Issue 17369: if charset/lang is None, decode_rfc2231 couldn't parse 53 | # the value, so use the fallback_charset. 54 | charset = fallback_charset 55 | rawbytes = bytes(text, 'raw-unicode-escape') 56 | try: 57 | return str(rawbytes, charset, errors) 58 | except LookupError: 59 | # charset is not a known codec. 60 | return text 61 | 62 | def patch(): 63 | old = utils.collapse_rfc2231_value 64 | utils.collapse_rfc2231_value = _collapse_rfc2231_value 65 | print("Overiding broken collapse_rfc2231_value") 66 | return old 67 | -------------------------------------------------------------------------------- /tools/push-failures.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # Licensed to the Apache Software Foundation (ASF) under one or more 4 | # contributor license agreements. See the NOTICE file distributed with 5 | # this work for additional information regarding copyright ownership. 6 | # The ASF licenses this file to You under the Apache License, Version 2.0 7 | # (the "License"); you may not use this file except in compliance with 8 | # the License. You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | """ Utility for retrying docs that we failed to index earlier. 19 | """ 20 | 21 | import argparse 22 | import json 23 | import os 24 | from elastic import Elastic 25 | 26 | es = Elastic() 27 | 28 | parser = argparse.ArgumentParser(description='Command line options.') 29 | # Cannot have both source and mid as input 30 | parser.add_argument('--source', dest='dumpdir', 31 | help='Path to the directory containing the JSON documents that failed to index') 32 | 33 | args = parser.parse_args() 34 | 35 | dumpDir = args.dumpdir if args.dumpdir else '.' 36 | 37 | print("Looking for *.json files in %s" % dumpDir) 38 | 39 | files = [f for f in os.listdir(dumpDir) if os.path.isfile(os.path.join(dumpDir, f)) and f.endswith('.json')] 40 | 41 | for f in files: 42 | fpath = os.path.join(dumpDir, f) 43 | print("Processing %s" % fpath) 44 | with open(fpath, "r") as f: 45 | ojson = json.load(f) 46 | if 'mbox' in ojson and 'mbox_source' in ojson: 47 | try: 48 | mid = ojson['id'] 49 | except KeyError: 50 | mid = ojson['mbox']['mid'] 51 | es.index( 52 | doc_type="mbox", 53 | id=mid, 54 | body = ojson['mbox'] 55 | ) 56 | 57 | es.index( 58 | doc_type="mbox_source", 59 | id=mid, 60 | body = ojson['mbox_source'] 61 | ) 62 | 63 | if 'attachments' in ojson and ojson['attachments']: 64 | for k, v in ojson['attachments'].items(): 65 | es.index( 66 | doc_type="attachment", 67 | id=k, 68 | body = { 69 | 'source': v 70 | } 71 | ) 72 | f.close() 73 | os.unlink(fpath) 74 | print ("All done! Pushed %u documents to ES." % len(files)) 75 | -------------------------------------------------------------------------------- /aaa_examples/aaa_with_subgroups.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Licensed to the Apache Software Foundation (ASF) under one or more 3 | contributor license agreements. See the NOTICE file distributed with 4 | this work for additional information regarding copyright ownership. 5 | The ASF licenses this file to You under the Apache License, Version 2.0 6 | (the "License"); you may not use this file except in compliance with 7 | the License. You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | ]]-- 17 | 18 | -- This is aaa.lua - AAA filter with portal validation. 19 | -- It checks that an oauth validated account was logged in through a specific 20 | -- OAuth provider, and if used, allows access to private lists. 21 | -- To use this as your AAA lib, replace aaa.lua in site/api/lib with this file. 22 | 23 | local config = require 'lib/config' 24 | 25 | -- Allow anyone logged in through Google+ access to private emails 26 | -- This is a direct string match, not a GLOB 27 | local valid_portal = "www.googleapis.com" 28 | 29 | -- Grant specific email addresses access to specific private areas. 30 | -- This is either (sub)domain specific or list specific. 31 | -- Lists must follow the List-ID format: listname.domain.tld 32 | -- Thus internal.foocorp.com can point to either internal@foocorp.com or 33 | -- *@internal.foocorp.com, use with care! 34 | local access_list = { 35 | ['luca@foocorp.com'] = { 36 | "internal.foocorp.com", -- grant access to *@internal.foocorp.com 37 | "hr.foocorp.com" -- grant access to hr@foocorp.com 38 | }, 39 | ['donna@foocorp.com'] = { 40 | 'hr.foocorp.com', -- hr@foocorp.com 41 | 'legal.foocorp.com' -- *@legal.foorcop.com (or legal@foorcorp.com, depends :p) 42 | }, 43 | ['eric@foocorp.com'] = { 44 | '*' -- grant access to everything! 45 | } 46 | } 47 | 48 | -- Get rights (full or no access) 49 | local function getRights(r, usr) 50 | local email = usr.credentials.email or "|||" 51 | local xemail = email:match("([-a-zA-Z0-9._@]+)") -- whitelist characters 52 | local rights = {} 53 | 54 | -- bad char in email? 55 | if not email or xemail ~= email then 56 | return rights 57 | end 58 | 59 | -- Check if the access list has this email on file, and if so, 60 | -- return the access list for that specific email account 61 | if access_list[email] then 62 | rights = access_list[email] 63 | end 64 | return rights 65 | end 66 | 67 | -- module defs 68 | return { 69 | validateParams = true, 70 | rights = getRights 71 | } 72 | -------------------------------------------------------------------------------- /site/api/notifications.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Licensed to the Apache Software Foundation (ASF) under one or more 3 | contributor license agreements. See the NOTICE file distributed with 4 | this work for additional information regarding copyright ownership. 5 | The ASF licenses this file to You under the Apache License, Version 2.0 6 | (the "License"); you may not use this file except in compliance with 7 | the License. You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | ]]-- 17 | 18 | -- This is notifications.lua - a script for fetching up to 50 email notifications 19 | -- also marks an email as seen if required 20 | 21 | local JSON = require 'cjson' 22 | local elastic = require 'lib/elastic' 23 | local aaa = require 'lib/aaa' 24 | local user = require 'lib/user' 25 | local cross = require 'lib/cross' 26 | local utils = require 'lib/utils' 27 | 28 | function handle(r) 29 | cross.contentType(r, "application/json") 30 | local get = r:parseargs() 31 | 32 | -- make sure we're logged in 33 | local account = user.get(r) 34 | if account then 35 | 36 | -- callback from the browser when the user has viewed an email. mark it as seen. 37 | if get.seen then 38 | local mid = get.seen 39 | if mid and #mid > 0 then 40 | local doc = elastic.get("notifications", mid) 41 | if doc and doc.seen then 42 | elastic.update("notifications", doc.request_id, { seen = 1 }) 43 | r:puts[[{"marked": true}]] 44 | return cross.OK 45 | end 46 | end 47 | r:puts[[{}]] 48 | return cross.OK 49 | end 50 | local peml = {} 51 | 52 | -- Find all recent notification docs, up to 50 latest results 53 | local docs = elastic.find("recipient:\"" .. r:sha1(account.cid) .. "\"", 50, "notifications") 54 | for k, doc in pairs(docs) do 55 | -- if we can see the email, push the notif to the list 56 | if aaa.canAccessDoc(r, doc, account) then 57 | doc.id = doc['message-id'] 58 | doc.tid = doc.id 59 | doc.nid = doc.request_id 60 | doc.irt = doc['in-reply-to'] 61 | table.insert(peml, doc) 62 | end 63 | end 64 | -- spit out JSON 65 | r:puts(JSON.encode{ 66 | notifications = peml 67 | }) 68 | else 69 | r:puts[[{}]] 70 | end 71 | return cross.OK 72 | end 73 | 74 | cross.start(handle) -------------------------------------------------------------------------------- /test/resources/pass/text-enriched.mbox: -------------------------------------------------------------------------------- 1 | From tomcat-user-return-34660-apmail-jakarta-tomcat-user-archive=jakarta.apache.org@jakarta.apache.org Mon May 14 19:23:38 2001 2 | Return-Path: 3 | Delivered-To: apmail-jakarta-tomcat-user-archive@jakarta.apache.org 4 | Received: (qmail 71623 invoked by uid 500); 14 May 2001 19:23:25 -0000 5 | Mailing-List: contact tomcat-user-help@jakarta.apache.org; run by ezmlm 6 | Precedence: bulk 7 | Reply-To: tomcat-user@jakarta.apache.org 8 | list-help: 9 | list-unsubscribe: 10 | list-post: 11 | List-Id: 12 | Delivered-To: mailing list tomcat-user@jakarta.apache.org 13 | Received: (qmail 71599 invoked from network); 14 May 2001 19:23:07 -0000 14 | Received: from netgate.emmes.com (root@208.208.19.150) 15 | by h31.sny.collab.net with SMTP; 14 May 2001 19:23:07 -0000 16 | Received: from kumar ([10.0.0.229]) 17 | by netgate.emmes.com (8.9.3/8.9.3) with SMTP id OAA09624 18 | for ; Mon, 14 May 2001 14:58:19 -0400 19 | Message-Id: <3.0.5.32.20010514152321.0090b230@netgate.emmes.com> 20 | X-Sender: kumar@netgate.emmes.com 21 | X-Mailer: QUALCOMM Windows Eudora Pro Version 3.0.5 (32) 22 | Date: Mon, 14 May 2001 15:23:21 -0400 23 | To: tomcat-user@jakarta.apache.org 24 | From: Kumar Thotapally 25 | Subject: Virtual Hosts 26 | Mime-Version: 1.0 27 | Content-Type: text/enriched; charset="us-ascii" 28 | X-Spam-Rating: h31.sny.collab.net 1.6.2 0/1000/N 29 | 30 | Hi, 31 | 32 | 33 | I am able to startup and shutdown multiple JVMs (using server1.xml ... 34 | etc). I created applications (for example, JVM1, JVM2) under web-apps. 35 | 36 | 37 | When I enter http://localhost:8007/jvm1/servlet/HelloWorld1 in my 38 | browser, 39 | 40 | it works fine. 41 | 42 | 43 | However, with the following code in server1.xml : 44 | 45 | 46 | Courier New < 48 | 49 | 50 | 51 | followed by 52 | 53 | Courier New < 55 | 56 | 57 | < 58 | 59 | <Courier New 66 | debug="1" 67 | 68 | reloadable="true" > 69 | 70 | < 71 | 72 | < 73 | 74 | along with other contexts. 75 | 76 | 77 | When I enter www.tomcat1.com, I am not able to get the result. 78 | 79 | 80 | Any suggestions? 81 | 82 | 83 | Thanks, 84 | 85 | 86 | Kumar. 87 | 88 | -------------------------------------------------------------------------------- /site/api/lib/cross.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Licensed to the Apache Software Foundation (ASF) under one or more 3 | contributor license agreements. See the NOTICE file distributed with 4 | this work for additional information regarding copyright ownership. 5 | The ASF licenses this file to You under the Apache License, Version 2.0 6 | (the "License"); you may not use this file except in compliance with 7 | the License. You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | ]]-- 17 | 18 | -- cross-server module for making apache and nginx work roughly the same way 19 | 20 | local function setContentType(r, foo) 21 | if ngx and ngx.header then 22 | ngx.header['Content-Type'] = foo 23 | else 24 | r.content_type = foo 25 | end 26 | end 27 | 28 | _M = {} 29 | 30 | local apr = nil 31 | pcall(function() apr = require 'apr' end) 32 | 33 | local function ngstart(handler) 34 | if ngx then 35 | _G.apache2 = { 36 | OK = 0 37 | } 38 | local r = { 39 | puts = function(r, ...) ngx.say(...) end, 40 | write = function(r, ...) ngx.say(...) end, 41 | md5 = function(r, foo) return ngx.md5(foo) end, 42 | clock = ngx.time, 43 | parseargs = function() return ngx.req.get_uri_args() end, 44 | parsebody = function() 45 | ngx.req.read_body() 46 | return ngx.req.get_post_args() 47 | end, 48 | getcookie = function(r, name) return ngx.var['cookie_' .. name] end, 49 | setcookie = function(r, tbl) 50 | ngx.header["Set-Cookie"] = ("%s=%s; Path=/;"):format(tbl.key, tbl.value) 51 | end, 52 | escape = function(r, foo) return ngx.escape_uri(foo) end, 53 | unescape = function(r, foo) return ngx.unescape_uri(foo) end, 54 | sha1 = function(r, foo) return apr and apr.sha1(foo) or ngx.md5(foo) end, 55 | ivm_set = function(r, key, val) _M['ivm_' .. key] = val end, 56 | ivm_get = function(r, key) return _M['ivm_' .. key] end, 57 | hostname = ngx.var['http_host'], 58 | strcmp_match = function(str, pat) 59 | pat = pat:gsub("%.", "%%."):gsub("*", ".+") 60 | return str:match(pat) 61 | end, 62 | useragent_ip = ngx.var.remote_addr, 63 | base64_decode = function(r, foo) return ngx.decode_base64(foo) end, 64 | headers_out = ngx.header, 65 | port = 443 -- I don't know where to fetch this in nginx :( 66 | } 67 | handler(r) 68 | end 69 | end 70 | 71 | return { 72 | contentType = setContentType, 73 | start = ngstart, 74 | OK = apache2 and apache2.OK or 0 75 | } -------------------------------------------------------------------------------- /aaa_examples/aaa_by_email_address.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Licensed to the Apache Software Foundation (ASF) under one or more 3 | contributor license agreements. See the NOTICE file distributed with 4 | this work for additional information regarding copyright ownership. 5 | The ASF licenses this file to You under the Apache License, Version 2.0 6 | (the "License"); you may not use this file except in compliance with 7 | the License. You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | ]]-- 17 | 18 | -- This is aaa.lua - AAA filter with email validation. 19 | -- It checks that an oauth validated email account matches *@foocorp.com and if 20 | -- so, grants access to private emails. 21 | -- To use this as your AAA lib, replace aaa.lua in site/api/lib with this file. 22 | 23 | local config = require 'lib/config' 24 | 25 | -- validated emails ending with @foocorp.com have access to all private emails 26 | -- This is a GLOB, so could also be *@internal.foocorp.com, or *-admin@foocorp.com etc 27 | -- This AAA module requires strcmp_match which is only found in Apache httpd currently. 28 | local valid_email = "*@foocorp.com" 29 | local grant_access_to = "*" -- use * for access to all, or specify a (sub)domain to grant access to 30 | local useAlternates = false -- also check against alternate email addresses?? 31 | 32 | -- Is email a valid foocorp email? 33 | local function validateEmail(r, email) 34 | -- do a GLOB match, testing email aginst valid_email 35 | if r:strcmp_match(valid_email, email) then 36 | return true 37 | end 38 | return false 39 | end 40 | 41 | 42 | -- Get a list of domains the user has private email access to (or wildcard if org member) 43 | local function getRights(r, usr) 44 | local email = usr.credentials.email or "|||" 45 | local xemail = email:match("([-a-zA-Z0-9._@]+)") -- whitelist characters 46 | local rights = {} 47 | 48 | -- bad char in email? 49 | if not email or xemail ~= email then 50 | return rights 51 | end 52 | 53 | -- first, check against primary address 54 | local validEmail = validateEmail(r, email) 55 | 56 | -- if enabled, check against alternates 57 | if useAlternates then 58 | if usr and usr.credentials and type(usr.credentials.altemail) == "table" then 59 | for k, v in pairs(usr.credentials.altemail) do 60 | if validateEmail(r, v.email) then 61 | validEmail = true 62 | break 63 | end 64 | end 65 | end 66 | end 67 | 68 | -- Check if email matches foocorp.com 69 | if usr.internal.admin or validateEmail(r, email) then 70 | table.insert(rights, grant_access_to) 71 | end 72 | return rights 73 | end 74 | 75 | -- module defs 76 | return { 77 | validateParams = true, 78 | rights = getRights 79 | } 80 | -------------------------------------------------------------------------------- /test/resources/pass/emptybody.mbox: -------------------------------------------------------------------------------- 1 | From issues-return-129-apmail-ponymail-issues-archive=ponymail.apache.org@ponymail.incubator.apache.org Thu Nov 17 00:49:30 2016 2 | Return-Path: 3 | X-Original-To: apmail-ponymail-issues-archive@minotaur.apache.org 4 | Delivered-To: apmail-ponymail-issues-archive@minotaur.apache.org 5 | Received: from mail.apache.org (hermes.apache.org [140.211.11.3]) 6 | by minotaur.apache.org (Postfix) with SMTP id A72D919611 7 | for ; Thu, 17 Nov 2016 00:49:30 +0000 (UTC) 8 | Received: (qmail 22868 invoked by uid 500); 17 Nov 2016 00:49:30 -0000 9 | Delivered-To: apmail-ponymail-issues-archive@ponymail.apache.org 10 | Received: (qmail 22841 invoked by uid 500); 17 Nov 2016 00:49:30 -0000 11 | Mailing-List: contact issues-help@ponymail.incubator.apache.org; run by ezmlm 12 | Precedence: bulk 13 | List-Help: 14 | List-Unsubscribe: 15 | List-Post: 16 | List-Id: 17 | Reply-To: dev@ponymail.incubator.apache.org 18 | Delivered-To: mailing list issues@ponymail.incubator.apache.org 19 | Received: (qmail 22832 invoked by uid 99); 17 Nov 2016 00:49:30 -0000 20 | Received: from pnap-us-west-generic-nat.apache.org (HELO spamd2-us-west.apache.org) (209.188.14.142) 21 | by apache.org (qpsmtpd/0.29) with ESMTP; Thu, 17 Nov 2016 00:49:30 +0000 22 | Received: from localhost (localhost [127.0.0.1]) 23 | by spamd2-us-west.apache.org (ASF Mail Server at spamd2-us-west.apache.org) with ESMTP id 2614B1A00A2 24 | for ; Thu, 17 Nov 2016 00:49:30 +0000 (UTC) 25 | X-Virus-Scanned: Debian amavisd-new at spamd2-us-west.apache.org 26 | X-Spam-Flag: NO 27 | X-Spam-Score: -7.019 28 | X-Spam-Level: 29 | X-Spam-Status: No, score=-7.019 tagged_above=-999 required=6.31 30 | tests=[KAM_LAZY_DOMAIN_SECURITY=1, RCVD_IN_DNSWL_HI=-5, 31 | RCVD_IN_MSPIKE_H3=-0.01, RCVD_IN_MSPIKE_WL=-0.01, 32 | RP_MATCHES_RCVD=-2.999] autolearn=disabled 33 | Received: from mx1-lw-eu.apache.org ([10.40.0.8]) 34 | by localhost (spamd2-us-west.apache.org [10.40.0.9]) (amavisd-new, port 10024) 35 | with ESMTP id XffM2rPGCqQk for ; 36 | Thu, 17 Nov 2016 00:49:29 +0000 (UTC) 37 | Received: from mail.apache.org (hermes.apache.org [140.211.11.3]) 38 | by mx1-lw-eu.apache.org (ASF Mail Server at mx1-lw-eu.apache.org) with SMTP id DD1285FCC9 39 | for ; Thu, 17 Nov 2016 00:49:28 +0000 (UTC) 40 | Received: (qmail 22820 invoked by uid 99); 17 Nov 2016 00:49:28 -0000 41 | Received: from minotaur.apache.org (HELO minotaur.apache.org) (140.211.11.9) 42 | by apache.org (qpsmtpd/0.29) with ESMTP; Thu, 17 Nov 2016 00:49:28 +0000 43 | Received: by minotaur.apache.org (Postfix, from userid 1721) 44 | id D69A719610; Thu, 17 Nov 2016 00:49:27 +0000 (UTC) 45 | To: issues@ponymail.incubator.apache.org 46 | Subject: Test email with empty body 47 | Message-Id: <20161117004927.D69A719610@minotaur.apache.org> 48 | Date: Thu, 17 Nov 2016 00:49:27 +0000 (UTC) 49 | From: sebb@apache.org (Sebastian Bazley) 50 | 51 | 52 | -------------------------------------------------------------------------------- /site/js/alts.js: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one or more 3 | contributor license agreements. See the NOTICE file distributed with 4 | this work for additional information regarding copyright ownership. 5 | The ASF licenses this file to You under the Apache License, Version 2.0 6 | (the "License"); you may not use this file except in compliance with 7 | the License. You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | 18 | // Callback that sets up the user menu in JS, provided 19 | // valid account JSON is supplied 20 | function setupUserAlts(json) { 21 | if (typeof json.login != undefined && json.login) { 22 | login = json.login 23 | if (login.credentials) { 24 | setupUser(json.login) 25 | } 26 | } 27 | renderAlts(json) 28 | } 29 | 30 | 31 | // Func for rendering the list of notifications 32 | function renderAlts(json) { 33 | if (json.login && json.login.credentials) { 34 | var obj = document.getElementById('alts') 35 | var main = json.login.credentials.email.replace(/<>"'/g, "") 36 | obj.innerHTML = main + " - primary address
" 37 | for (var i in json.login.alternates) { 38 | var alt = json.login.alternates[i] 39 | alt = alt.replace(/<>"'/g, "") 40 | obj.innerHTML += alt + " - [Remove]
" 41 | } 42 | } else { 43 | var obj = document.getElementById('alts') 44 | obj.innerHTML = "You need to be logged in to manage alternate addresses." 45 | } 46 | 47 | } 48 | 49 | // onLoad function, fetches the needed JSON and renders the notif list 50 | // invoked by onload in merge.html 51 | function listAlts() { 52 | GetAsync("/api/preferences.lua", null, setupUserAlts) 53 | } 54 | 55 | function processNewAlt(json) { 56 | if (json.requested) { 57 | if (json.requested == 1) { 58 | alert("An association request has been sent to the specified email address." + 59 | " Please check your inbox! Depending on grey-listing etc, it may take up to 15 minutes before your confirmation email arrives.") 60 | } else { 61 | alert(json.requested) 62 | } 63 | } else { 64 | if (json.error) { 65 | alert(json.error) 66 | } else { 67 | alert("Unexpected response from server" + JSON.stringify(json)) 68 | } 69 | } 70 | } 71 | function newAlt(addr) { 72 | if (addr.match(/^([^\s@]+@[^\s@]+)$/) && addr.length > 6) { 73 | GetAsync("/api/preferences.lua?associate=" + addr, null, processNewAlt) 74 | } else { 75 | alert("Please enter a valid email address!") 76 | } 77 | 78 | return false; 79 | } 80 | 81 | function delAlt(addr) { 82 | GetAsync("/api/preferences.lua?removealt=" + addr, null, function() { location.href = location.href; }) 83 | return false; 84 | } -------------------------------------------------------------------------------- /site/oauth.html: -------------------------------------------------------------------------------- 1 | 2 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | Pony Mail! 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 34 | 35 | 36 | 37 |
38 | 39 |
40 |
41 | 42 |
43 |
44 |

45 |

Log in using one of the following identity providers:

46 |
47 |

48 |

49 | 50 |

51 |
52 | 53 |
54 |
55 |
56 | 57 | 58 |
59 |
 
60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /tools/feedwrapper.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Licensed to the Apache Software Foundation (ASF) under one or more 3 | # contributor license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright ownership. 5 | # The ASF licenses this file to You under the Apache License, Version 2.0 6 | # (the "License"); you may not use this file except in compliance with 7 | # the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | """ 17 | This is feedwrapper - a mailing list auto-subscriber and/or feed passthrough program. 18 | Activate it by adding "|/usr/bin/env python3 /path/to/ponymail/tools/feedwrapper.py localuser@thisdomain.abc" 19 | Then subscribe to lists by running: python3 feedwrapper sub localuser@thisdomain.abc ml-subscribe@mldomain.foo" 20 | """ 21 | 22 | import sys, re, os, email, smtplib 23 | from subprocess import Popen, PIPE 24 | path = os.path.dirname(os.path.realpath(__file__)) 25 | 26 | if __name__ == '__main__': 27 | if len(sys.argv) <= 1: 28 | print("Usage: feedwrapper [recipient email] OR") 29 | print(" feedwrapper sub [recipient] [ML-subscribe-address]") 30 | sys.exit(0) 31 | if sys.argv[1] == "sub": 32 | sender = sys.argv[2] 33 | recip = sys.argv[3] 34 | smtpObj = smtplib.SMTP('localhost') 35 | smtpObj.sendmail(sender, [recip], """From: %s 36 | To: %s 37 | Subject: subscribe 38 | 39 | subscribe 40 | """ % (sender, recip) 41 | ) 42 | print("Sent subscription request for %s to %s" % (sender, recip)) 43 | else: 44 | msg = email.message_from_file(sys.stdin) 45 | 46 | if msg.get('to') and msg.get('reply-to') and msg.get('subject'): 47 | if msg.get('to').find(sys.argv[1]) != -1 and \ 48 | re.search(r"-request@", msg.get('reply-to')) or \ 49 | (\ 50 | re.match(r"confirm subscribe to", msg.get('subject'), flags=re.IGNORECASE) and \ 51 | re.search(r"-sc\.", msg.get('reply-to')) \ 52 | ): 53 | with open("%s/wrapper.log" % path, "a") as f: 54 | f.write("%s - %s: %s\n" % (msg.get('to'), msg.get('reply-to'), msg.get('subject'))) 55 | f.write("We've got a subscription request for %s. \n" % msg.get('reply-to')) 56 | 57 | smtpObj = smtplib.SMTP('localhost') 58 | smtpObj.sendmail(sys.argv[1], [msg.get('reply-to')], """From: %s 59 | To: %s 60 | Subject: %s 61 | 62 | %s 63 | """ % (sys.argv[1], msg.get('reply-to'), msg.get('subject'), msg.get('subject')) 64 | ) 65 | else: 66 | with open("%s/wrapper.log" % path, "a") as f: 67 | f.write("Got an email for %s\n" % (msg.get('list-id') or "??")) 68 | f.write("%s - %s: %s\n" % (msg.get('to'), msg.get('reply-to'), msg.get('subject'))) 69 | p = Popen("/usr/bin/python3 %s/../mm3/plugin.py" % path, shell=True, stdin=PIPE, stderr=PIPE, stdout=sys.stdout) 70 | print(p.communicate(input=msg.as_string().encode('utf-8'))) 71 | p.stdin.close() 72 | f.write("-----\n") 73 | -------------------------------------------------------------------------------- /site/permalink.html: -------------------------------------------------------------------------------- 1 | 2 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | Pony Mail! 21 | 22 | 23 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 37 | 38 | 39 | 40 |
41 | 42 | 44 |
45 | 46 | 47 |
48 |
49 | 50 | 51 | 52 | 54 | 55 | 56 | 57 | 58 |
 
59 | 66 | 67 | -------------------------------------------------------------------------------- /tools/install.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Licensed to the Apache Software Foundation (ASF) under one or more 3 | # contributor license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright ownership. 5 | # The ASF licenses this file to You under the Apache License, Version 2.0 6 | # (the "License"); you may not use this file except in compliance with 7 | # the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | import sys 18 | import getpass 19 | import subprocess 20 | import platform 21 | 22 | dname = platform.linux_distribution()[0].lower() 23 | dver = platform.linux_distribution()[1] 24 | 25 | if getpass.getuser() != "root": 26 | print("You need to run this script as root!") 27 | sys.exit(-1) 28 | 29 | print("Your distro seems to be : " + dname + " " + dver) 30 | 31 | if dname == 'ubuntu' or dname == 'debian': 32 | print("Running installation script for Debian/Ubuntu servers, hang on!") 33 | print("Installing pre-requisites via apt-get") 34 | subprocess.check_call(('apt-get', 'install', 'apache2', 'git', 'liblua5.2-dev', 'lua-cjson', 'lua-sec', 'lua-socket', 'python3', 'python3-pip', 'subversion')) 35 | 36 | print("Installing Python modules") 37 | subprocess.check_call(('pip3', 'install', 'elasticsearch', 'formatflowed')) 38 | 39 | print("Installing ElasticSearch") 40 | subprocess.check_call(('apt-get', 'install', 'openjdk-7-jre-headless')) 41 | 42 | try: 43 | subprocess.check_call(("wget -qO - https://packages.elastic.co/GPG-KEY-elasticsearch | sudo apt-key add -"), shell=True) 44 | subprocess.check_call(('echo "deb http://packages.elastic.co/elasticsearch/1.7/debian stable main" | sudo tee -a /etc/apt/sources.list.d/elasticsearch-1.7.list'), shell=True) 45 | except: 46 | print("Did we already add ES to the repo? hmm") 47 | 48 | subprocess.check_call(('apt-get', 'update')) 49 | subprocess.check_call(('apt-get', 'install', 'elasticsearch')) 50 | 51 | print("Checking out a copy of Pony Mail from GitHub") 52 | subprocess.check_call(('git', 'clone', 'https://github.com/Humbedooh/ponymail.git', '/var/www/ponymail')) 53 | 54 | print("Starting ElasticSearch") 55 | subprocess.check_call(('service', 'elasticsearch', 'start')) 56 | 57 | print("Writing httpd configuration file /etc/apache2/sites-enabled/99-ponymail.conf") 58 | with open("/etc/apache2/sites-enabled/99-ponymail.conf", "w") as f: 59 | f.write(""" 60 | 61 | ServerName mylists.foo.tld 62 | DocumentRoot /var/www/ponymail/site 63 | AddHandler lua-script .lua 64 | LuaScope thread 65 | LuaCodeCache stat 66 | AcceptPathInfo On 67 | """) 68 | 69 | if dname == 'ubuntu' and dver == '14.04': 70 | print("Ubuntu 14.04 specific step; Compiling mod_lua") 71 | subprocess.check_call(('apt-get', 'install', 'apache2-dev')) 72 | subprocess.check_call(('svn', 'co', 'https://svn.apache.org/repos/asf/httpd/httpd/branches/2.4.x/modules/lua/', '/tmp/mod_lua')) 73 | subprocess.check_call(("cd /tmp/mod_lua && apxs2 -I/usr/include/lua5.2 -cia mod_lua.c lua_*.c -lm -llua5.2"), shell=True) 74 | 75 | print("Enabling mod_lua") 76 | subprocess.check_call(('a2enmod', 'lua')) 77 | 78 | print("Starting httpd") 79 | subprocess.check_call(('service', 'apache2', 'start')) 80 | 81 | print("Done! Please run setup.py now to set up Pony Mail") 82 | 83 | -------------------------------------------------------------------------------- /site/ngrams.html: -------------------------------------------------------------------------------- 1 | 2 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | Pony Mail! 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 34 | 35 | 36 | 46 | 47 |
48 |
49 |
50 |
51 |
52 |
53 | Fetching trend data, hang on..!
54 |
55 |
56 |
57 |
58 | 59 |
60 |
61 | 62 |
 
63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /site/js/dev/ponymail_assign_vars.js: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one or more 3 | contributor license agreements. See the NOTICE file distributed with 4 | this work for additional information regarding copyright ownership. 5 | The ASF licenses this file to You under the Apache License, Version 2.0 6 | (the "License"); you may not use this file except in compliance with 7 | the License. You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | 18 | // These are all variables needed at some point during our work. 19 | // They keep track of the JSON we have received, storing it in the browser, 20 | // Thus lightening the load on the backend (caching and such) 21 | 22 | var _VERSION_ = "0.12-SNAPSHOT" // Current version (as far as we know) 23 | var months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] 24 | var d_ppp = 15; // results per page 25 | var c_page = 0; // current page position for list view 26 | var open_emails = [] // cache index for loaded emails 27 | var list_year = {} 28 | var DEFAULT_RETENTION = "lte=1M" // default timespan for list view 29 | var current_retention = DEFAULT_RETENTION 30 | var current_cal_min = 1997 // don't go further back than 1997 in case everything blows up, date-wise 31 | var keywords = "" 32 | var current_thread = 0 // marker for list view; currently open thread/email 33 | var current_thread_mids = {} // duplicate guard for threading 34 | var saved_emails = {} // JSON cache for emails 35 | var current_query = "" // currently active search query 36 | var old_json = {} // pointer to previously loaded JSON object 37 | var all_lists = {} 38 | var current_json = {} // pointer to currently loaded JSON 39 | var current_thread_json = {} 40 | var current_flat_json = {} 41 | var current_email_msgs = [] 42 | var current_reply_eid = null 43 | var last_opened_email = null 44 | var firstVisit = true 45 | var global_deep = false 46 | var old_state = {} 47 | var nest = "" 48 | var xlist = "" 49 | var domlist = {} 50 | var compose_headers = {} 51 | var login = {} 52 | var xyz 53 | var start = new Date().getTime() 54 | var latestEmailInThread = 0 55 | var composeType = "reply" 56 | var gxdomain = "" 57 | var fl = null 58 | var kiddos = [] // DOM tree for traverse functions 59 | var pending_urls = {} // URL list for GetAsync's support functions (such as the spinner) 60 | var pb_refresh = 0 61 | var treeview_guard = {} 62 | var mbox_month = null 63 | 64 | var URL_BASE = pm_config.URLBase ? pm_config.URLBase.replace(/\/+/g, "/") : "" 65 | 66 | function isStorageAvailable(type) { 67 | try { 68 | var storage = window[type], 69 | x = 'pm_test'; 70 | storage.setItem(x, x); 71 | storage.removeItem(x); 72 | return true; 73 | } 74 | catch(e) { 75 | return false; 76 | } 77 | } 78 | 79 | var localStorageAvailable = isStorageAvailable('localStorage') 80 | var sessionStorageAvailable = isStorageAvailable('sessionStorage') 81 | 82 | // Links from viewmode to the function that handles them 83 | var viewModes = { 84 | threaded: { 85 | email: loadEmails_threaded, 86 | list: loadList_threaded, 87 | description: 'Grouped by threads' 88 | }, 89 | flat: { 90 | email: loadEmails_flat, 91 | list: loadList_flat, 92 | description: 'Flat list (one email per line)' 93 | }, 94 | treeview: { 95 | email: loadEmails_flat, 96 | list: loadList_treeview, 97 | description: 'Threaded with treeview' 98 | }, 99 | } 100 | -------------------------------------------------------------------------------- /site/trends.html: -------------------------------------------------------------------------------- 1 | 2 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | Pony Mail! 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 35 | 36 | 37 | 47 | 48 |
49 |
50 |
51 | 54 |
55 |
56 | 57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 | 65 |
66 |
67 | 68 |
 
69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /tools/list-lists.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # Licensed to the Apache Software Foundation (ASF) under one or more 4 | # contributor license agreements. See the NOTICE file distributed with 5 | # this work for additional information regarding copyright ownership. 6 | # The ASF licenses this file to You under the Apache License, Version 2.0 7 | # (the "License"); you may not use this file except in compliance with 8 | # the License. You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | import time 19 | import argparse 20 | import json 21 | 22 | from elastic import Elastic 23 | 24 | dbname=None 25 | 26 | parser = argparse.ArgumentParser(description='Command line options.') 27 | parser.add_argument('--dbname', dest='dbname', type=str, 28 | help='Override index name') 29 | parser.add_argument('--pretty', dest='pretty', action='store_true', 30 | help='Convert List IDs to email addresses') 31 | parser.add_argument('--debug', dest='debug', action='store_true', 32 | help='Output the result JSON instead, very noisy!') 33 | parser.add_argument('--counts', dest='counts', action='store_true', 34 | help='Show the count of messages for each list') 35 | 36 | args = parser.parse_args() 37 | 38 | dbname = args.dbname 39 | 40 | then = time.time() 41 | 42 | # get config and set up default database 43 | # If dbname is None, the config setting will be used 44 | es = Elastic(dbname=dbname) 45 | 46 | page = es.search( 47 | doc_type="mbox", 48 | size = 0, 49 | body = { 50 | 'aggs': { 51 | 'lists': { 52 | 'terms': { 53 | 'field': "list_raw", 54 | 'size': 500000 55 | }, 56 | 'aggs': { 57 | 'privacy' : { 58 | 'filter' : {# are there any private messages? 59 | 'term': { 60 | 'private': True 61 | } 62 | } 63 | } 64 | } 65 | } 66 | }, 67 | 'query': { 68 | 'bool': { 69 | 'must': [ 70 | { 71 | 'range': { 72 | 'date': { 73 | 'lt': "now+2d" 74 | } 75 | } 76 | } 77 | ] 78 | } 79 | } 80 | } 81 | ) 82 | 83 | plist = {} 84 | total_private = 0 85 | if args.debug: 86 | print(json.dumps(page)) 87 | else: 88 | for domain in page['aggregations']['lists']['buckets']: 89 | listid = domain['key'] 90 | msgcount = domain['doc_count'] 91 | prvcount = domain['privacy']['doc_count'] 92 | total_private += prvcount 93 | if args.pretty: 94 | if listid.find(".") != -1: 95 | l, d = listid.strip("<>").split(".", 1) 96 | plist[d] = plist[d] if d in plist else {} 97 | plist[d][l]=[msgcount, prvcount] 98 | else: 99 | if args.counts: 100 | print(listid, msgcount, prvcount) 101 | else: 102 | print(listid) 103 | 104 | for dom in sorted(plist): 105 | for ln in sorted(plist[dom]): 106 | if args.counts: 107 | print("%s@%s %d %d" % (ln, dom, plist[dom][ln][0], plist[dom][ln][1])) 108 | else: 109 | print("%s@%s" % (ln, dom)) 110 | if args.counts: 111 | print("Total messages %d of which private %d" % (page['hits']['total'], total_private)) 112 | -------------------------------------------------------------------------------- /tools/mboxo_patch.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Licensed to the Apache Software Foundation (ASF) under one or more 3 | # contributor license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright ownership. 5 | # The ASF licenses this file to You under the Apache License, Version 2.0 6 | # (the "License"); you may not use this file except in compliance with 7 | # the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | """ 18 | Byte stream reader to process mboxo style mailbox files. 19 | These are not currently handled by the Python email package. 20 | 21 | It replaces any occurrence of b'\n>From ' with b'\nFrom ' 22 | 23 | The class handles matching across read boundaries. 24 | 25 | To use: 26 | 27 | from mboxo_patch import MboxoFactory 28 | ... 29 | messages = mailbox.mbox(filename, MboxoFactory) 30 | 31 | N.B. 32 | To simplify the code, the MboxoReader class changes the 33 | size parameter to 7 if (and only if): 0 <= size < 7 34 | The return byte buffer can thus be larger than expected. 35 | However this is only a theoretical possibility 36 | as the mailbox code uses a size of 8192 (or None) 37 | 38 | """ 39 | import mailbox 40 | 41 | FROM_MANGLED =b'\n>From ' 42 | FROM_MANGLED_LEN=len(FROM_MANGLED) 43 | FROM_UNMANGLED=b'\nFrom ' 44 | # We want to match the 7 bytes b'\n>From ' in the input stream 45 | # However this can be split over multiple reads. 46 | # The split can occur anywhere after the leading b'\n' 47 | # and the trailing b' '. If we match any of these 48 | # we keep the trailing part of the buffer for next time 49 | # The following are all the possible prefixes for a split: 50 | FROMS=(FROM_MANGLED[:-1], 51 | FROM_MANGLED[:-2], 52 | FROM_MANGLED[:-3], 53 | FROM_MANGLED[:-4], 54 | FROM_MANGLED[:-5], 55 | FROM_MANGLED[:-6], 56 | ) 57 | 58 | class MboxoReader(mailbox._PartialFile): # pylint: disable=W0212 59 | def __init__(self, f, start=None, stop=None): 60 | self.remain=0 # number of bytes to keep for next read 61 | super().__init__(f._file, start=f._start, stop=f._stop) # pylint: disable=W0212 62 | 63 | # Override the read method to provide mboxo filtering 64 | def _read(self, size, read_method): 65 | # get the next chunk, resetting if necessary 66 | if self.remain != 0: 67 | super().seek(whence=1, offset=-self.remain) 68 | # if size is None or negative, then read returns everything. 69 | # in which case there is no need to wory about matching across reads 70 | limited_read = size and size >= 0 71 | # ensure we get enough to match successfully when refilling 72 | if limited_read and size < FROM_MANGLED_LEN: 73 | size = FROM_MANGLED_LEN 74 | buff = super()._read(size, read_method) 75 | bufflen=len(buff) 76 | # did we get anything new? 77 | if limited_read and bufflen > self.remain: 78 | # is there a potential cross-boundary match? 79 | if buff.endswith(FROMS): 80 | # yes, work out what to keep 81 | # N.B. rindex will fail if it cannot find the LF; 82 | # this should be impossible 83 | self.remain=bufflen - buff.rindex(b'\n') 84 | else: 85 | # don't need to keep anything back 86 | self.remain=0 87 | else: 88 | # EOF 89 | self.remain=0 90 | # we cannot use -0 to mean end of array... 91 | end = bufflen if self.remain == 0 else -self.remain 92 | # exclude the potential split match from the return 93 | return buff[:end].replace(FROM_MANGLED, FROM_UNMANGLED) 94 | 95 | class MboxoFactory(mailbox.mboxMessage): 96 | def __init__(self, message=None): 97 | super().__init__(message=MboxoReader(message)) 98 | -------------------------------------------------------------------------------- /test/stats_cli.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env lua 2 | 3 | --[[ 4 | Licensed to the Apache Software Foundation (ASF) under one or more 5 | contributor license agreements. See the NOTICE file distributed with 6 | this work for additional information regarding copyright ownership. 7 | The ASF licenses this file to You under the Apache License, Version 2.0 8 | (the "License"); you may not use this file except in compliance with 9 | the License. You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | ]]-- 19 | 20 | -- Allow CLI testing of stats.lua query parameters 21 | -- Invoke with pairs of parameters, being the key and value, e.g. 22 | -- lua test_stats.lua list dev domain ponymail.apache.org q 'a/b' d lte=1y emailsOnly 1 23 | -- Note that parameters with no values are given the default value of "1" by mod_lua 24 | -- set MODE=inspect to print output query from inspect, otherwise print as JSON suitable for further processing 25 | 26 | -- Update path so we can always find the api/*.lua scripts 27 | local self=arg[0] or './dummy' -- get path to self 28 | local pfx=self:match("^.*/") or "" -- extract the path 29 | -- finally update package path (also adding current dir) 30 | package.path = package.path .. ";" .. pfx .. "../site/api/?.lua" -- path to api dir 31 | package.path = package.path .. ";" .. pfx .. "/?.lua" -- current dir 32 | 33 | local inspect = require 'inspect' 34 | local http = require 'socket.http' 35 | local mock = require 'mock_r' 36 | local JSON = require 'cjson' 37 | require 'stats' -- local makes no difference here 38 | 39 | local _CACHE = {} -- capture output 40 | 41 | -- override http request so can capture the query 42 | http.request = function(url, data) 43 | -- capture HTTP parameters (assume only called once) 44 | _CACHE.url = url 45 | _CACHE.querydata = JSON.decode(data) 46 | -- return simplest result that satisfies stats.lua 47 | result = [[ 48 | { 49 | "hits" : { 50 | "total" : 0, 51 | "hits" : [ ] 52 | } 53 | } 54 | ]] 55 | return result, 200 56 | end 57 | 58 | 59 | local r = mock.r 60 | 61 | -- disable years active check 62 | r.ivm_get = function(r, key) 63 | return JSON.encode({ pubfirst = 0, publast = 0}) 64 | end 65 | 66 | -- collect output (assume only one call to puts) 67 | r.puts = function(r, ...) _CACHE.reply = JSON.decode(...) end 68 | 69 | -- TODO 70 | r.escape_html = function(r, val) 71 | -- < > & are definitely escaped by the real escape_html 72 | return val:gsub('>', '>'):gsub('<', '<'):gsub('&', '&') 73 | end 74 | 75 | -- override the parse-args function so it returns our test data 76 | r.parseargs = function(r) 77 | return _CACHE.args 78 | end 79 | 80 | 81 | local function test(args) 82 | local output = { 83 | quick = true, -- disable most queries 84 | } 85 | -- merge in user data 86 | for k,v in pairs(args) do output[k] = v end 87 | _CACHE.args = output -- save the args 88 | _CACHE.status = handle(r) 89 | return _CACHE 90 | end 91 | 92 | local argc = #arg 93 | if argc == 0 -- assume reading lines of JSON strings 94 | then 95 | for line in io.lines() 96 | do 97 | jzon = JSON.decode(line) 98 | jzon.quick = true -- disable most queries 99 | _CACHE.args = jzon 100 | _CACHE.status = handle(r) 101 | print(JSON.encode(_CACHE)) 102 | io.flush() 103 | end 104 | elseif argc % 2 == 0 105 | then 106 | local data = {} 107 | for i = 1,argc,2 do 108 | data[arg[i]] = arg[i+1] 109 | end 110 | res = test(data) 111 | if os.getenv("MODE") == "inspect" then 112 | print(inspect(res["querydata"])) 113 | else 114 | print(JSON.encode(res)) 115 | end 116 | elseif argc == 1 -- assume JSON string 117 | then 118 | jzon = JSON.decode(arg[1]) 119 | jzon.quick = true -- disable most queries 120 | _CACHE.args = jzon 121 | _CACHE.status = handle(r) 122 | print(JSON.encode(_CACHE)) 123 | else 124 | print("Need even arg count") 125 | os.exit(1) 126 | end 127 | -------------------------------------------------------------------------------- /aaa_examples/aaa_with_ldap.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Licensed to the Apache Software Foundation (ASF) under one or more 3 | contributor license agreements. See the NOTICE file distributed with 4 | this work for additional information regarding copyright ownership. 5 | The ASF licenses this file to You under the Apache License, Version 2.0 6 | (the "License"); you may not use this file except in compliance with 7 | the License. You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | ]] 17 | 18 | -- This is aaa_site.lua - site-specific AAA filter for ASF. 19 | 20 | local JSON = require 'cjson' 21 | 22 | -- Get a list of PMCs the user is a part of 23 | local function getPMCs(uid) 24 | local groups = {} 25 | -- Check for valid chars. Important since the uid is passed to the shell. 26 | if not uid:match("^[-a-z0-9_.]+$") then 27 | return groups 28 | end 29 | local ldapdata = io.popen( ([[ldapsearch -x -LLL -b ou=project,ou=groups,dc=apache,dc=org "(owner=uid=%s,ou=people,dc=apache,dc=org)" dn]]):format(uid) ) 30 | local data = ldapdata:read("*a") 31 | for match in data:gmatch("dn: cn=([-a-zA-Z0-9]+),ou=project,ou=groups,dc=apache,dc=org") do 32 | table.insert(groups, match) 33 | end 34 | return groups 35 | end 36 | 37 | 38 | -- Is $uid a member of the ASF? 39 | local function isMember(uid) 40 | -- Check for valid chars. Important since the uid is passed to the shell. 41 | if not uid:match("^[-a-z0-9_.]+$") then 42 | return false 43 | end 44 | local ldapdata = io.popen(([[ldapsearch -x -LLL -b cn=member,ou=groups,dc=apache,dc=org '(memberUid=%s)' dn]]):format(uid)) 45 | -- This returns a string starting with 'dn: cn=member,ou=groups,dc=apache,dc=org' or the empty string. 46 | local data = ldapdata:read("*a") 47 | return nil ~= data:match("dn: cn=member,ou=groups,dc=apache,dc=org") 48 | end 49 | 50 | -- Is $uid a committer of the ASF? 51 | local function isCommitter(uid) 52 | -- Check for valid chars. Important since the uid is passed to the shell. 53 | if not uid:match("^[-a-z0-9_.]+$") then 54 | return false 55 | end 56 | local ldapdata = io.popen(([[ldapsearch -x -LLL -b ou=people,dc=apache,dc=org '(uid=%s)' dn]]):format(uid)) 57 | -- This returns a string starting with 'dn: uid=uid,ou=people,dc=apache,dc=org' or the empty string. 58 | local data = ldapdata:read("*a") 59 | return nil ~= data:match(("dn: uid=%s,ou=people,dc=apache,dc=org"):format(uid)) 60 | end 61 | 62 | -- additional top-level lists (*.apache.org) to which committers are entitled 63 | local LISTS = {"committers", "list2"} -- etc 64 | 65 | -- Get a list of domains the user has private email access to (or wildcard if org member) 66 | local function getRights(r, usr) 67 | local uid = usr.credentials.uid 68 | 69 | -- First, check the 30 minute cache 70 | local NOWISH = math.floor(os.time() / 1800) 71 | local USER_KEY = "aaa_rights_" .. NOWISH .. "_" .. uid 72 | local t = r:ivm_get(USER_KEY) 73 | if t then 74 | return JSON.decode(t) 75 | end 76 | 77 | local rights = {} 78 | -- Check if uid has member (admin) rights 79 | if usr.internal.admin or isMember(uid) then 80 | table.insert(rights, "*") 81 | -- otherwise, get PMC list and construct array 82 | else 83 | -- Add the PMC lists 84 | local list = getPMCs(uid) 85 | for k, v in pairs(list) do 86 | table.insert(rights, v .. ".apache.org") 87 | end 88 | -- Add the lists for all committers 89 | if isCommitter(uid) then 90 | for k, v in ipairs(LISTS) do 91 | table.insert(rights, v .. ".apache.org") 92 | end 93 | end 94 | end 95 | r:ivm_set(USER_KEY, JSON.encode(rights)) 96 | return rights 97 | end 98 | 99 | -- module defs 100 | return { 101 | rights = getRights, 102 | validateParams = true 103 | } 104 | -------------------------------------------------------------------------------- /site/api/lib/utils.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Licensed to the Apache Software Foundation (ASF) under one or more 3 | contributor license agreements. See the NOTICE file distributed with 4 | this work for additional information regarding copyright ownership. 5 | The ASF licenses this file to You under the Apache License, Version 2.0 6 | (the "License"); you may not use this file except in compliance with 7 | the License. You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | ]]-- 17 | 18 | -- This is lib/utils.lua - utility methods 19 | 20 | local JSON = require 'cjson' -- for JSON.null 21 | 22 | local days = { -- days in months of the year 23 | -- Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec 24 | 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 25 | } 26 | 27 | -- find the original topic starter 28 | local function findParent(r, doc, elastic) 29 | local step = 0 30 | -- max 50 steps up in the hierarchy 31 | while step < 50 do 32 | step = step + 1 33 | local irt = doc['in-reply-to'] 34 | if not irt then 35 | break -- won't happen because irt is always present currently 36 | end 37 | -- Extract the reference, if any 38 | irt = irt:match("(<[^>]+>)") 39 | if not irt then 40 | break 41 | end 42 | local docs = elastic.find('message-id:"' .. r:escape(irt)..'"', 1, "mbox") 43 | if #docs == 0 then 44 | break 45 | end 46 | doc = docs[1] 47 | end 48 | return doc 49 | end 50 | 51 | 52 | 53 | --[[ 54 | Anonymize the document body 55 | ]] 56 | local function anonymizeBody(body) 57 | return body:gsub("<(%S+)@([-a-zA-Z0-9_.]+)>", function(a,b) return "<" .. a:sub(1,2) .. "..." .. "@" .. b .. ">" end) 58 | end 59 | 60 | --[[ 61 | Anonymize an email address 62 | ]] 63 | local function anonymizeEmail(email) 64 | return email:gsub("(%S+)@(%S+)", function(a,b) return a:sub(1,2) .. "..." .. "@" .. b end) 65 | end 66 | 67 | --[[ 68 | Anonymize document headers: 69 | - from 70 | - cc 71 | - to 72 | Also processes from_raw if specified 73 | ]] 74 | local function anonymizeHdrs(doc, from_raw) 75 | if doc.from and doc.from ~= JSON.null and #doc.from > 0 then 76 | doc.from = anonymizeEmail(doc.from) 77 | end 78 | if doc.cc and doc.cc ~= JSON.null and #doc.cc > 0 then 79 | doc.cc = anonymizeEmail(doc.cc) 80 | end 81 | if doc.to and doc.to ~= JSON.null and #doc.to > 0 then 82 | doc.to = anonymizeEmail(doc.to) 83 | end 84 | if from_raw and doc.from_raw then 85 | doc.from_raw = anonymizeEmail(doc.from_raw) 86 | end 87 | return doc 88 | end 89 | 90 | -- extract canonical email address from from field 91 | local function extractCanonEmail(from) 92 | local eml = from:match("<(.-)>") or from:match("%S+@%S+") or nil 93 | if eml == nil and from:match(".- at .- %(") then 94 | eml = from:match("(.- at .-) %("):gsub(" at ", "@") 95 | elseif eml == nil then 96 | eml = "unknown" 97 | end 98 | return eml 99 | end 100 | 101 | -- is it a leap year? 102 | local function leapYear(year) 103 | if (year % 4 == 0) then 104 | if (year%100 == 0) then 105 | if (year %400 == 0) then 106 | return true 107 | end 108 | else 109 | return true 110 | end 111 | return false 112 | end 113 | end 114 | 115 | -- get the last day of the month 116 | local function lastDayOfMonth(yyyy, mm) 117 | local ldom 118 | if mm == 2 and leapYear(yyyy) then 119 | ldom = 29 120 | else 121 | ldom = days[mm] 122 | end 123 | return ldom 124 | end 125 | 126 | return { 127 | MAX_LIST_COUNT = 500000, -- used for aggs size=n 128 | SHORTENED_LINK_LEN = 18, -- length of a shortened link 129 | anonymizeHdrs = anonymizeHdrs, 130 | anonymizeBody = anonymizeBody, 131 | anonymizeEmail = anonymizeEmail, 132 | extractCanonEmail = extractCanonEmail, 133 | findParent = findParent, 134 | leapYear = leapYear, 135 | lastDayOfMonth = lastDayOfMonth 136 | } 137 | -------------------------------------------------------------------------------- /site/js/config.js.sample: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one or more 3 | contributor license agreements. See the NOTICE file distributed with 4 | this work for additional information regarding copyright ownership. 5 | The ASF licenses this file to You under the Apache License, Version 2.0 6 | (the "License"); you may not use this file except in compliance with 7 | the License. You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | 18 | var pm_config = { 19 | debug: false, // set to true for some debug output 20 | oauth: { 21 | // OAuth settings 22 | 23 | /* Apache OAuth example 24 | apache: { 25 | name: "Apache OAuth", 26 | oauth_portal: "https://oauth.apache.org/", 27 | oauth_url: "https://oauth.apache.org/token", 28 | fullname_key: 'fullname', 29 | email_key: 'email' 30 | }, 31 | */ 32 | /* Google example 33 | google: { 34 | name: "Google OAuth", 35 | oauth_portal: "https://accounts.google.com/o/oauth2/auth", 36 | oauth_url: "https://www.googleapis.com/oauth2/v3/tokeninfo?id_token=", 37 | fullname_key: 'name', 38 | email_key: 'email', 39 | client_id: 'your.google.app.id.here' 40 | }, 41 | */ 42 | /* GitHub example 43 | * Remember to edit site/api/lib/config.lua and add GitHub to the oauth_fields array! 44 | github: { 45 | name: "GitHub OAuth", 46 | oauth_portal: "https://github.com/login/oauth/authorize", 47 | oauth_url: "https://github.com/login/oauth/access_token", 48 | fullname_key: 'name', 49 | email_key: 'email', 50 | construct: true, // needed for GitHub 51 | client_id: 'your.github.app.id.here', 52 | scope: "user:email" 53 | }, 54 | */ 55 | /* Basic Auth example with CAS/whatever 56 | * Remember to edit site/api/lib/config.lua and add 'localhost' to the oauth_fields array! 57 | internal: { 58 | name: "CAS Auth", 59 | oauth_portal: "/oauth.html?key=internal&", 60 | oauth_url: "/oauth.html?key=internal&", 61 | }, 62 | */ 63 | /* 64 | // OAuth.online 65 | online: { 66 | name: "OAuth.online", 67 | oauth_portal: "https://oauth.online/", 68 | oauth_url: "https://verify.oauth.online/token", 69 | } 70 | */ 71 | }, 72 | indexMode: 'table', // front page view mode: 73 | // phonebook: Standard phonebook mode, sort/list by domain name (a.org, b.org, c.org...) 74 | // phonebook_short: Same as above, but sort/list by list name (dev@a.org, dev@.org, user@a.org...) 75 | // table: A more detailed view meant for smaller list sites (<=20-30 lists or such) 76 | shortLists: true, // whether to display foo@bar.org or just foo@ in flat view 77 | shortLinks: false, // Whether to shorten links using base32 78 | URLBase: '', // Rewrite base for URLs. If you serve from http://foo.tld/ponymail/, set this to '/ponymail' 79 | } 80 | 81 | 82 | 83 | // Localized preferences (defaults) 84 | var prefs = { 85 | displayMode: 'threaded', // threaded or flat 86 | groupBy: 'thread', // thread or date 87 | sortOrder: 'forward', // forward or reverse sort 88 | compactQuotes: 'yes', // Show quotes from original email as compacted blocks? 89 | notifications: 'direct', // Notify on direct or indirect replies to your posts? 90 | hideStats: 'yes', // Hide the email statistics window? 91 | theme: 'compact', // Set to 'social' to default to the social theme 92 | autoScale: 'no', // Whether to scale results-per-page to window height (4K screens etc) 93 | loggedIn: false 94 | } 95 | 96 | // array of prefs we have now. This is needed in case we change/break the existing 97 | // structure saved in elasticsearch for users. Update when needed! 98 | var pref_keys = ['displayMode','groupBy','sortOrder','compactQuotes','notifications','hideStats','theme', 'fullname', 'autoScale'] 99 | -------------------------------------------------------------------------------- /test/generatortest.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # -*- coding: utf-8 -*- 4 | 5 | # Licensed to the Apache Software Foundation (ASF) under one or more 6 | # contributor license agreements. See the NOTICE file distributed with 7 | # this work for additional information regarding copyright ownership. 8 | # The ASF licenses this file to You under the Apache License, Version 2.0 9 | # (the "License"); you may not use this file except in compliance with 10 | # the License. You may obtain a copy of the License at 11 | # 12 | # http://www.apache.org/licenses/LICENSE-2.0 13 | # 14 | # Unless required by applicable law or agreed to in writing, software 15 | # distributed under the License is distributed on an "AS IS" BASIS, 16 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | # See the License for the specific language governing permissions and 18 | # limitations under the License. 19 | 20 | """ 21 | This file tests the generators against mbox files 22 | """ 23 | 24 | # PYTHONPATH is used to give access to archiver.py 25 | # PYTHONPATH=../tools python3 generatortest.py generatortest.yaml 26 | 27 | import mailbox 28 | import sys 29 | import os 30 | import yaml 31 | import subprocess 32 | from pprint import pprint 33 | from collections import namedtuple 34 | 35 | 36 | TOOLS = os.path.join(os.path.dirname(os.path.dirname(os.path.realpath(__file__))),"tools") 37 | sys.path.append(TOOLS) 38 | import archiver 39 | ARCHIVER=os.path.join(TOOLS,"archiver.py") 40 | import generators 41 | 42 | list_override = None # could affect id 43 | private = False # does not affect id generation 44 | parseHTML = False # can this affect id generation? 45 | GENS=generators.generator_names() 46 | 47 | archie = archiver.Archiver(parse_html = parseHTML) 48 | 49 | 50 | for arg in sys.argv[1:]: 51 | if arg.endswith('.yml') or arg.endswith('.yaml'): 52 | errors = 0 53 | with open(arg, 'r') as stream: 54 | data = yaml.safe_load(stream) 55 | for test in data['tests']: 56 | for file in test: 57 | print("Testing with %s" % file) 58 | mbox = mailbox.mbox(file, None, create=False) 59 | scripts = test[file] 60 | msgcnt = len(mbox) 61 | scrcnt = len(scripts) 62 | if msgcnt != scrcnt: 63 | print("WARN: mbox contains %d messages, but there are %d unit tests" % (msgcnt, scrcnt)) 64 | messages=iter(mbox) 65 | for script in scripts: 66 | if 'exit' in script: 67 | break 68 | if 'gen' in script: 69 | print("Generator %s" % script['gen']) 70 | archie.generator = script['gen'] 71 | message = next(messages) 72 | json, contents, _msgdata, _irt = archie.compute_updates(list_override, private, message) 73 | error = 0 74 | for key in script: 75 | if key == 'gen': 76 | continue 77 | if not key in json: 78 | print("key %s is not in response" % key) 79 | elif script[key] == json[key]: 80 | pass 81 | else: 82 | error = 1 83 | print("key %s\nexp: %s\nact: %s\n%s %s" % (key, script[key], json[key], json['date'], json['subject'])) 84 | errors += error 85 | print("Completed %d tests (%d errors)" % (scrcnt, errors)) 86 | elif arg.endswith('.mbox'): 87 | messages = mailbox.mbox(arg, None, create=False) 88 | for message in messages: 89 | print(message.get_from()) 90 | for gen in GENS: 91 | archie.generator = gen 92 | json, contents, _msgdata, _irt = archie.compute_updates(list_override, private, message) 93 | print("%15s: %s" % (gen,json['mid'])) 94 | elif arg.endswith('.eml'): # a single email 95 | for gen in GENS: 96 | with open(arg,'rb') as f: 97 | out = subprocess.run([ARCHIVER,"--dry","--generator",gen], stdin=f, capture_output=True, text=True) 98 | try: 99 | mid = out.stdout.splitlines()[1].strip('!').split()[-1] 100 | print("%15s: %s" % (gen,mid)) 101 | except: 102 | print(out.stdout) 103 | else: 104 | print("Unknown file type %s" % arg) 105 | -------------------------------------------------------------------------------- /site/js/dev/ponymail_timetravel.js: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one or more 3 | contributor license agreements. See the NOTICE file distributed with 4 | this work for additional information regarding copyright ownership. 5 | The ASF licenses this file to You under the Apache License, Version 2.0 6 | (the "License"); you may not use this file except in compliance with 7 | the License. You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | 18 | 19 | // simple func that just redirects to the original thread URL we just got if possible 20 | function timeTravelSingleThreadRedirect(json) { 21 | if (json && json.emails[0]) { 22 | location.href = URL_BASE + "/thread.html/" + (pm_config.shortLinks ? shortenID(json.emails[0].mid) : encodeURIComponent(json.emails[0].mid)) 23 | } 24 | } 25 | 26 | // Func that fetches the timetravel data for the current thread (permalink mode) 27 | function timeTravelSingleThread() { 28 | var mid = current_thread_json[0].mid 29 | GetAsync("/api/thread.lua?timetravel=true&id=" + mid, null, timeTravelSingleThreadRedirect) 30 | } 31 | 32 | 33 | 34 | // time travel in list view mode, callback from the API: 35 | function timeTravelListRedirect(json, state) { 36 | if (json && json.emails) { 37 | for (var i in json.emails) { 38 | current_flat_json.push(json.emails[i]) 39 | } 40 | } 41 | // Did we receive timetravel data? 42 | if (json && json.emails[0]) { 43 | var osubs = countSubs(current_thread_json[state.id]) 44 | var nsubs = countSubs(json.emails[0]) 45 | var oid = current_thread_json[state.id].tid 46 | 47 | // Did we actually get more emails now than we had before? 48 | if (nsubs > osubs || nsubs >= osubs && !json.emails[0].irt) { 49 | if (prefs.displayMode == 'threaded') { 50 | toggleEmails_threaded(state.id) 51 | current_thread_json[state.id] = json.emails[0] 52 | toggleEmails_threaded(state.id) 53 | } else if (prefs.displayMode == 'treeview') { 54 | toggleEmails_treeview(state.id) 55 | current_thread_json[state.id] = json.emails[0] 56 | toggleEmails_treeview(state.id) 57 | } 58 | var subs = countSubs(json.emails[0]) 59 | var parts = countParts(json.emails[0]) 60 | // If we have subs/people labels available, change them and set the newly found stats 61 | if (document.getElementById('subs_' + state.id) != null) { 62 | document.getElementById('subs_' + state.id).innerHTML = " " + subs + " replies" 63 | document.getElementById('people_' + state.id).innerHTML = " " + parts + " people" 64 | document.getElementById('people_' + state.id).style.visibility = parts > 1 ? "visible" : "hidden" 65 | } 66 | // Note to user whether we found something new or not 67 | document.getElementById('magic_' + state.id).innerHTML = "Voila! We've found the oldest email in this thread for you and worked our way forward. Enjoy!" 68 | } 69 | // Nope, nothing new - bummer! 70 | else { 71 | document.getElementById('magic_' + state.id).innerHTML = "Hm, we couldn't find any more messages in this thread. bummer!" 72 | } 73 | // Should we jump in the HTML to somewhere? 74 | if (state.jump) { 75 | var thread = findEpoch(state.jump) 76 | if (thread) { 77 | thread.setAttribute("meme", "true") 78 | thread.style.background = "rgba(200,200,255, 0.25)" 79 | xyz = thread.getAttribute("id") 80 | window.setTimeout(function() { document.getElementById(xyz).scrollIntoView() }, 1000) 81 | document.getElementById(xyz).scrollIntoView() 82 | } else { 83 | document.getElementById('magic_' + state.id).scrollIntoView(); 84 | } 85 | document.getElementById('magic_' + state.id).innerHTML = "Showing the thread in its entirety" 86 | } 87 | current_thread_json[state.id].magic = true 88 | } 89 | } 90 | 91 | // time travel inside a list view 92 | function timeTravelList(id, jump) { 93 | var mid = current_thread_json[id].tid 94 | GetAsync("/api/thread.lua?timetravel=true&id=" + mid, {id: id, jump: jump}, timeTravelListRedirect) 95 | } 96 | -------------------------------------------------------------------------------- /test/resources/valid/test_288.mbox: -------------------------------------------------------------------------------- 1 | From tomcat-user-return-48043-qmlist-jakarta-archive-tomcat-user=jakarta.apache.org@jakarta.apache.org Thu Jan 09 19:29:05 2003 2 | Return-Path: 3 | Delivered-To: apmail-jakarta-tomcat-user-archive@apache.org 4 | Received: (qmail 1133 invoked from network); 9 Jan 2003 19:29:05 -0000 5 | Received: from exchange.sun.com (192.18.33.10) 6 | by daedalus.apache.org with SMTP; 9 Jan 2003 19:29:05 -0000 7 | Received: (qmail 17497 invoked by uid 97); 9 Jan 2003 19:29:48 -0000 8 | Delivered-To: qmlist-jakarta-archive-tomcat-user@jakarta.apache.org 9 | Received: (qmail 17474 invoked by uid 97); 9 Jan 2003 19:29:47 -0000 10 | Mailing-List: contact tomcat-user-help@jakarta.apache.org; run by ezmlm 11 | Precedence: bulk 12 | List-Unsubscribe: 13 | List-Subscribe: 14 | List-Help: 15 | List-Post: 16 | List-Id: "Tomcat Users List" 17 | Reply-To: "Tomcat Users List" 18 | Delivered-To: mailing list tomcat-user@jakarta.apache.org 19 | Received: (qmail 17459 invoked by uid 98); 9 Jan 2003 19:29:47 -0000 20 | X-Antivirus: nagoya (v4218 created Aug 14 2002) 21 | X-MimeOLE: Produced By Microsoft Exchange V6.0.6249.0 22 | content-class: urn:content-classes:message 23 | Subject: RE: Tomcat w/Apache Question 24 | Date: Thu, 9 Jan 2003 14:28:01 -0500 25 | Message-ID: 26 | Thread-Topic: Tomcat w/Apache Question 27 | Thread-Index: AcK4EhnGZKzpRY5kRZmqrUSPyAtbzwAAyDmQ 28 | From: "Wilson, Allen" 29 | To: "Tomcat Users List" 30 | MIME-Version: 1.0 31 | Content-Type: multipart/mixed; boundary="<<001-3e1dcd5a-119e>>" 32 | Content-Disposition: inline 33 | Content-Transfer-Encoding: 8bit 34 | X-Spam-Rating: daedalus.apache.org 1.6.2 0/1000/N 35 | X-Spam-Rating: daedalus.apache.org 1.6.2 0/1000/N 36 | 37 | --<<001-3e1dcd5a-119e>> 38 | Content-type: text/plain 39 | Content-transfer-encoding: 8bit 40 | 41 | Lajos... 42 | 43 | ...this should definitely help....thanks... 44 | 45 | Allen 46 | 47 | -----Original Message----- 48 | From: Lajos Moczar [mailto:lmocz@galatea.com] 49 | Sent: Thursday, January 09, 2003 1:05 PM 50 | To: Tomcat Users List 51 | Subject: Re: Tomcat w/Apache Question 52 | 53 | 54 | Allen - 55 | 56 | I have some guides on my site, http://www.galatea.com/flashguides, the 57 | describe the details of Apache-Tomcat integration. Let me know if they help. 58 | 59 | Regards, 60 | 61 | Lajos 62 | 63 | 64 | Wilson, Allen wrote: 65 | > Has anyone successfully configured Tomcat to with with Apache using the AJP connector. 66 | > 67 | > I am presently trying to set up the connectors with Apache 1.3.20 and everything was 68 | > going find until I tried to do the portion for configuring mod_jk.so with the Apache 69 | > version. 70 | > 71 | > I received several errors in reference to a C header file socketvar.h. 72 | > 73 | > If someone could provide some insight on what the problem is or information on successful 74 | > configuring Apache and Tomcat to work together (I am presently using information out of 75 | > the Wrox Professional Apache Tomcat book). It would be appreciated 76 | > 77 | > Thank you.... 78 | > Allen 79 | > 80 | > 81 | > ------------------------------------------------------------------------ 82 | > 83 | > This message may contain proprietary or confidential company information. 84 | > Any unauthorized use or disclosure is prohibited. 85 | > 86 | > 87 | > 88 | > 89 | > ------------------------------------------------------------------------ 90 | > 91 | > -- 92 | > To unsubscribe, e-mail: 93 | > For additional commands, e-mail: 94 | 95 | 96 | -- 97 | galatea.com 98 | Cocoon training, consulting & support 99 | 100 | 101 | -- 102 | To unsubscribe, e-mail: 103 | For additional commands, e-mail: 104 | 105 | 106 | --<<001-3e1dcd5a-119e>> 107 | Content-type: text/plain 108 | Content-transfer-encoding: 8bit 109 | 110 | This message may contain proprietary or confidential company information. 111 | Any unauthorized use or disclosure is prohibited. 112 | 113 | 114 | 115 | --<<001-3e1dcd5a-119e>> 116 | Content-Type: text/plain; charset=us-ascii 117 | 118 | -- 119 | To unsubscribe, e-mail: 120 | For additional commands, e-mail: 121 | --<<001-3e1dcd5a-119e>>-- 122 | 123 | 124 | -------------------------------------------------------------------------------- /site/js/weburl.js: -------------------------------------------------------------------------------- 1 | // 2 | // Regular Expression for URL validation 3 | // 4 | // Author: Diego Perini 5 | // Updated: 2010/12/05 6 | // License: MIT 7 | // 8 | // Copyright (c) 2010-2013 Diego Perini (http://www.iport.it) 9 | // 10 | // Permission is hereby granted, free of charge, to any person 11 | // obtaining a copy of this software and associated documentation 12 | // files (the "Software"), to deal in the Software without 13 | // restriction, including without limitation the rights to use, 14 | // copy, modify, merge, publish, distribute, sublicense, and/or sell 15 | // copies of the Software, and to permit persons to whom the 16 | // Software is furnished to do so, subject to the following 17 | // conditions: 18 | // 19 | // The above copyright notice and this permission notice shall be 20 | // included in all copies or substantial portions of the Software. 21 | // 22 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 23 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 24 | // OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 25 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 26 | // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 27 | // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 28 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 29 | // OTHER DEALINGS IN THE SOFTWARE. 30 | // 31 | // the regular expression composed & commented 32 | // could be easily tweaked for RFC compliance, 33 | // it was expressly modified to fit & satisfy 34 | // these test for an URL shortener: 35 | // 36 | // http://mathiasbynens.be/demo/url-regex 37 | // 38 | // Notes on possible differences from a standard/generic validation: 39 | // 40 | // - utf-8 char class take in consideration the full Unicode range 41 | // - TLDs have been made mandatory so single names like "localhost" fails 42 | // - protocols have been restricted to ftp, http and https only as requested 43 | // 44 | // Changes: 45 | // 46 | // - IP address dotted notation validation, range: 1.0.0.0 - 223.255.255.255 47 | // first and last IP address of each class is considered invalid 48 | // (since they are broadcast/network addresses) 49 | // 50 | // - Added exclusion of private, reserved and/or local networks ranges 51 | // 52 | // - Made starting path slash optional (http://example.com?foo=bar) 53 | // 54 | // - Allow a dot (.) at the end of hostnames (http://example.com.) 55 | // 56 | // Compressed one-line versions: 57 | // 58 | // Javascript version 59 | // 60 | // /^(?:(?:https?|ftp):\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,}))\.?)(?::\d{2,5})?(?:[/?#]\S*)?$/i 61 | // 62 | // PHP version 63 | // 64 | // _^(?:(?:https?|ftp)://)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\x{00a1}-\x{ffff}0-9]-*)*[a-z\x{00a1}-\x{ffff}0-9]+)(?:\.(?:[a-z\x{00a1}-\x{ffff}0-9]-*)*[a-z\x{00a1}-\x{ffff}0-9]+)*(?:\.(?:[a-z\x{00a1}-\x{ffff}]{2,}))\.?)(?::\d{2,5})?(?:[/?#]\S*)?$_iuS 65 | // 66 | var re_weburl = new RegExp( 67 | "(" + 68 | // protocol identifier 69 | "(?:(?:https?|ftp)://)" + 70 | // user:pass authentication 71 | "(?:\\S+(?::\\S*)?@)?" + 72 | "(?:" + 73 | // IP address exclusion 74 | // private & local networks 75 | "(?!(?:10|127)(?:\\.\\d{1,3}){3})" + 76 | "(?!(?:169\\.254|192\\.168)(?:\\.\\d{1,3}){2})" + 77 | "(?!172\\.(?:1[6-9]|2\\d|3[0-1])(?:\\.\\d{1,3}){2})" + 78 | // IP address dotted notation octets 79 | // excludes loopback network 0.0.0.0 80 | // excludes reserved space >= 224.0.0.0 81 | // excludes network & broacast addresses 82 | // (first & last IP address of each class) 83 | "(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])" + 84 | "(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}" + 85 | "(?:\\.(?:[1-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))" + 86 | "|" + 87 | // host name 88 | "(?:(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)" + 89 | // domain name 90 | "(?:\\.(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)*" + 91 | // TLD identifier 92 | "(?:\\.(?:[a-z\\u00a1-\\uffff]{2,}))" + 93 | // TLD may end with dot 94 | "\\.?" + 95 | ")" + 96 | // port number 97 | "(?::\\d{2,5})?" + 98 | // resource path 99 | "(?:[/?#]([^,<>()\\[\\] \\t\\r\\n]|(<[^:\\s]*?>|\\([^:\\s]*?\\)|\\[[^:\\s]*?\\]))*)?" + 100 | ")\\.?" 101 | , "mig" 102 | ); -------------------------------------------------------------------------------- /site/notifications.html: -------------------------------------------------------------------------------- 1 | 2 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | Pony Mail! 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 35 | 36 | 37 | 63 | 64 |
65 | 66 |
67 | 68 | 70 |
71 | 76 |

Notifications

77 |
78 | Loading notifications, please hang on... 79 |
80 | 81 |
82 |
83 |
84 | 85 |
 
86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /RELEASE-NOTES.md: -------------------------------------------------------------------------------- 1 | # Apache Pony Mail (Incubating) 2 | 3 | Release Notes 4 | 5 | # Version 0.12 (not yet released) # 6 | 7 | The archiver no longer adds an 'archive-at' header to incoming messages. 8 | This does not affect the source stored in the database, as it uses the raw input (since v0.10) 9 | Generators that use the header -- full, medium (if Date not available) -- are now consistent 10 | 11 | Inline attachements are now parsed. This affects the output of the cluster generator. 12 | 13 | # Version 0.11 # 14 | 15 | No changes that require additional setup. 16 | 17 | # Version 0.10 # 18 | 19 | ### Fixing null favorite entries ### 20 | 21 | There was a bug in the preferences API that could result in one or more null entries 22 | being added to the favorite mailing lists for an account. This would prevent the '*' tab from displaying. 23 | The bug has been fixed which will prevent further nulls from being created. 24 | There is a script - tools/nullfav.py - which can be used to clean up existing favorites. 25 | Run as follows: 26 | 27 | $ python3 nullfav.py # list affected accounts 28 | 29 | If there are any, then: 30 | 31 | $ python3 nullfav.py --apply # apply changes to affected accounts 32 | 33 | If the database is busy it's possible that some accounts may fail to update. 34 | If so, just run the script again. 35 | 36 | To confirm that all the accounts have been fixed, run the script again: 37 | 38 | $ python3 nullfav.py 39 | 40 | Once this has been done, there should be no need to run the script again. 41 | 42 | ### Change to AAA ### 43 | 44 | Pony Mail now has two AAA modules: 45 | - lib/aaa.lua 46 | - lib/aaa_site.lua 47 | 48 | The aaa.lua file is now a generic module which implements the AAA API. 49 | It does not grant any rights; that must be done by the aaa_site.lua module. 50 | 51 | Before updating an existing installation, copy aaa.lua to aaa_site.lua otherwise it will be overwritten. 52 | The new generic aaa.lua module will automatically use aaa_site.lua if present. 53 | 54 | If this is a new installation, the lib/aaa_site.lua module needs to be created. 55 | There are several examples in the aaa_examples directory or you can create your own. 56 | 57 | This was done to simplify subsequent releases. 58 | 59 | ### Significant changes to GUI ### 60 | 61 | - mixed public/private lists are now displayed in the menu 62 | - improved display of quoted material in messages 63 | - better handling of missing/empty Subjects and bodies 64 | - better handling of broken mail threads 65 | - dates are all displayed in UTC 66 | - improved error reporting including for missing / inaccessible links 67 | - flat view mode now shows first line of body (as for threaded views) 68 | - search panel is updated with current month when selection changes 69 | 70 | ### Significant changes to functionality ### 71 | 72 | - private messages are now included in archive downloads if the user has access to them 73 | - various improvements to the archiver/importer: 74 | - better handling of encodings, including attachment names 75 | - handles more attachment types 76 | - handles more text types 77 | - can import individual mbox files 78 | - better error handling when communicating with the ES server 79 | - setup.py now sets up all mappings 80 | - stored dates are now all in UTC 81 | - API modules no longer return unnecessary data, reducing network traffic 82 | 83 | ### Potentially incompatible changes ### 84 | 85 | - mbox_source messages are now stored as base64 encoded text if they cannot be stored as ASCII 86 | See #366. 87 | This only affects the backend database contents, as the data is decoded as necessary on fetch. 88 | 89 | - the archiver and importer now generate the same MID for identical messages 90 | In version 0.9, the archiver and importer could generate different MIDs for the same message. 91 | This has been fixed, however it means that messages archived with 0.9 may have a different MID from 92 | the same message archived - or imported - with 0.10. 93 | Messages imported with 0.10 will have the same MID as messages imported with 0.9 94 | It is only the 0.9 archiver that could generate different MIDs. 95 | 96 | ### Restrictions/Known bugs ### 97 | 98 | ------ 99 | There are unresolved design issues with the existing id generators. 100 | 101 | The original and medium generators don't generate unique ids, so not 102 | all distinct emails can be archived. 103 | The full generator probably generates unique ids, however these are not 104 | guaranteed stable, so re-importing mail may cause duplicates to be archived. 105 | 106 | Since Permalinks currently rely on the generated ids, there is no guarantee 107 | that Permalinks are unique or permanent. 108 | ------ 109 | 110 | - HTML-only mails are not archived unless the Python `html2text` package (GPLv3) is installed and the `--html2text` command line arg is used 111 | 112 | - there are no proper unit tests 113 | 114 | - the 'full' generator uses the same format as the 'medium' generator for the ids 115 | -------------------------------------------------------------------------------- /site/api/thread.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Licensed to the Apache Software Foundation (ASF) under one or more 3 | contributor license agreements. See the NOTICE file distributed with 4 | this work for additional information regarding copyright ownership. 5 | The ASF licenses this file to You under the Apache License, Version 2.0 6 | (the "License"); you may not use this file except in compliance with 7 | the License. You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | ]]-- 17 | 18 | -- This is thread.lua - a script for fetching a thread based on a message 19 | -- that is in said thread. 20 | 21 | local JSON = require 'cjson' 22 | local elastic = require 'lib/elastic' 23 | local aaa = require 'lib/aaa' 24 | local user = require 'lib/user' 25 | local cross = require 'lib/cross' 26 | local config = require 'lib/config' 27 | local utils = require 'lib/utils' 28 | 29 | local emls_thrd 30 | 31 | -- func that fetches all children of an original topic email thingy 32 | local function fetchChildren(r, pdoc, c, biglist, account) 33 | c = (c or 0) + 1 34 | -- don't fetch more than 250 subtrees, we don't want to nest ad nauseam 35 | if c > 250 then 36 | return {} 37 | end 38 | -- biglist is for making sure we dont' fetch something twice 39 | biglist = biglist or {} 40 | local children = {} 41 | -- find any emails that reference this one 42 | local emid = r:escape(pdoc['message-id']) 43 | local docs = elastic.findFast( ('in-reply-to:"%s" OR references:"%s"'):format(emid, emid), 50, "mbox") 44 | for k, doc in pairs(docs) do 45 | -- if we haven't seen this email before, check for its kids and add it to the bunch 46 | if (not biglist[doc['message-id']]) and aaa.canAccessDoc(r, doc, account) then 47 | biglist[doc['message-id']] = true 48 | local mykids = fetchChildren(r, doc, c, biglist, account) 49 | if not account and config.antispam then 50 | doc = utils.anonymizeHdrs(doc) 51 | end 52 | local dc = { 53 | tid = doc.mid, 54 | mid = doc.mid, 55 | subject = doc.subject, 56 | from = doc.from, 57 | id = doc.request_id, 58 | epoch = doc.epoch, 59 | children = mykids, 60 | irt = doc['in-reply-to'] 61 | } 62 | table.insert(children, dc) 63 | table.insert(emls_thrd, dc) 64 | else 65 | biglist[doc['message-id']] = true 66 | docs[k] = nil 67 | end 68 | end 69 | return children 70 | end 71 | 72 | function handle(r) 73 | cross.contentType(r, "application/json; charset=UTF-8") 74 | local DEBUG = config.debug or false 75 | local START = DEBUG and r:clock() or nil 76 | local get = r:parseargs() 77 | -- get the parameter (if any) and tidy it up 78 | local eid = (get.id or ""):gsub("\"", "") 79 | -- If it is the empty string then set it to "1" so ES doesn't barf 80 | -- N.B. ?id is treated as ?id=1 81 | if #eid == 0 then eid = "1" end 82 | local doc = elastic.get("mbox", eid, true) 83 | emls_thrd = {} 84 | -- Try searching by mid if not found, for backward compat 85 | if not doc or not doc.mid then 86 | local docs = elastic.find("message-id:\"" .. r:escape(eid) .. "\"", 1, "mbox") 87 | if #docs == 0 and #eid == utils.SHORTENED_LINK_LEN then 88 | docs = elastic.find("mid:" .. r:escape(eid) .. "*", 1, "mbox") 89 | end 90 | if #docs == 1 then 91 | doc = docs[1] 92 | end 93 | end 94 | if get.timetravel then 95 | doc = utils.findParent(r, doc, elastic) 96 | end 97 | local doclist = {} 98 | 99 | -- did we find an email? 100 | if doc then 101 | local account = user.get(r) 102 | if doc and doc.mid and aaa.canAccessDoc(r, doc, account) then 103 | if not account and config.antispam then 104 | doc = utils.anonymizeHdrs(doc) 105 | end 106 | table.insert(emls_thrd, doc) 107 | doc.children = fetchChildren(r, doc, 1, nil, account) 108 | doc.tid = doc.mid 109 | doc.id = doc.request_id 110 | --doc.body = nil 111 | r:puts(JSON.encode({ 112 | took = DEBUG and (r:clock() - START) or nil, 113 | emails = emls_thrd, 114 | })) 115 | return cross.OK 116 | end 117 | end 118 | r:puts(JSON.encode{error = "No such e-mail or you do not have access to it."}) 119 | return cross.OK 120 | end 121 | 122 | cross.start(handle) 123 | -------------------------------------------------------------------------------- /site/css/solar.css: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one or more 3 | contributor license agreements. See the NOTICE file distributed with 4 | this work for additional information regarding copyright ownership. 5 | The ASF licenses this file to You under the Apache License, Version 2.0 6 | (the "License"); you may not use this file except in compliance with 7 | the License. You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | 18 | html { 19 | font-family: sans-serif; 20 | } 21 | 22 | body { 23 | margin: 0; 24 | } 25 | 26 | a:active, a:focus { 27 | color: #268bd2; 28 | } 29 | 30 | a:active, 31 | a:hover { 32 | outline: 0; 33 | } 34 | 35 | h1 { 36 | font-size: 2em; 37 | } 38 | 39 | code, 40 | kbd, 41 | pre { 42 | font-family: monospace, serif; 43 | font-size: 1em; 44 | } 45 | 46 | pre { 47 | white-space: pre-wrap; 48 | word-wrap: break-word; 49 | } 50 | 51 | q { 52 | quotes: "\201C" "\201D" "\2018" "\2019"; 53 | } 54 | 55 | small { 56 | font-size: 80%; 57 | } 58 | 59 | sub, 60 | sup { 61 | font-size: 75%; 62 | line-height: 0; 63 | position: relative; 64 | vertical-align: baseline; 65 | } 66 | 67 | fieldset { 68 | border: 1px solid #c0c0c0; 69 | margin: 0 2px; 70 | padding: 0.35em 0.625em 0.75em; 71 | } 72 | 73 | legend { 74 | border: 0; 75 | padding: 0; 76 | } 77 | 78 | button, 79 | input, 80 | select, 81 | textarea { 82 | font-family: inherit; 83 | font-size: 100%; 84 | margin: 0; 85 | } 86 | 87 | button, 88 | input { 89 | line-height: normal; 90 | } 91 | 92 | button, 93 | 94 | input[type="checkbox"], 95 | input[type="radio"] { 96 | box-sizing: border-box; 97 | padding: 0; 98 | } 99 | 100 | button::-moz-focus-inner, 101 | input::-moz-focus-inner { 102 | border: 0; 103 | padding: 0; 104 | } 105 | 106 | textarea { 107 | overflow: auto; 108 | vertical-align: top; 109 | } 110 | 111 | table { 112 | border-collapse: collapse; 113 | border-spacing: 0; 114 | } 115 | 116 | html { 117 | font-family: sans-serif; 118 | } 119 | 120 | pre, 121 | code { 122 | font-family: sans-serif; 123 | } 124 | 125 | h1, 126 | h2, 127 | h3, 128 | h4, 129 | h5, 130 | h6 { 131 | font-family: sans-serif; 132 | font-weight: 700; 133 | } 134 | 135 | html { 136 | background-color: #073642; 137 | color: #839496; 138 | margin: 1em; 139 | } 140 | 141 | body { 142 | background-color: #002b36; 143 | margin: 0 auto; 144 | border: 1pt solid #586e75; 145 | padding: 1em; 146 | } 147 | 148 | .well { 149 | background-color: #586e75; 150 | } 151 | 152 | .well li { 153 | background-color: #44423F; 154 | } 155 | 156 | .well li:hover { 157 | background-color: #54524F; 158 | } 159 | 160 | .bubble-info, .bubble-warning, .bubble-danger, .bubble-success, .bubble-primary, .bubble-topic { 161 | background: #2D2C2B !important; 162 | color: #e5b900; 163 | } 164 | 165 | .reply { 166 | background-color: #002b36; 167 | border: 1pt solid #586e75; 168 | box-shadow: 5pt 5pt 8pt #073642; 169 | color: #839496; 170 | padding: 1em; 171 | } 172 | 173 | #stats { 174 | background-color: #586e75; 175 | margin: 10px; 176 | border-radius: 5px; 177 | border: 1pt solid #889ea5; 178 | } 179 | 180 | code { 181 | background-color: #073642; 182 | padding: 2px; 183 | } 184 | 185 | a { 186 | color: #b58900; 187 | } 188 | 189 | a:visited { 190 | color: #cb4b16; 191 | } 192 | 193 | a:hover { 194 | color: #cb4b16; 195 | } 196 | 197 | h1 { 198 | color: #d33682; 199 | } 200 | 201 | h2, 202 | h3, 203 | h4, 204 | h5, 205 | h6 { 206 | color: #859900; 207 | } 208 | 209 | pre { 210 | background-color: #002b36; 211 | color: #839496; 212 | border: 1pt solid #586e75; 213 | padding: 1em; 214 | box-shadow: 5pt 5pt 8pt #073642; 215 | } 216 | 217 | pre code { 218 | background-color: #002b36; 219 | } 220 | 221 | h1 { 222 | font-size: 2.8em; 223 | } 224 | 225 | h2 { 226 | font-size: 2.4em; 227 | } 228 | 229 | h3 { 230 | font-size: 1.8em; 231 | } 232 | 233 | h4 { 234 | font-size: 1.4em; 235 | } 236 | 237 | .navbar { 238 | background: #374C44 !important; 239 | color: #DDD !important; 240 | margin-top: -1em; 241 | } 242 | 243 | .active a { 244 | background: #360 !important; 245 | color: #EEE !important; 246 | } 247 | 248 | .nav a:hover { 249 | color: #EEE !important; 250 | } 251 | 252 | .nav .open a { 253 | background: #360 !important; 254 | color: #EEE !important; 255 | } 256 | 257 | .nav .open a:hover { 258 | background: #360 !important; 259 | color: #FFF !important; 260 | font-weight: bold; 261 | } 262 | 263 | .footer { 264 | background: #374C44 !important; 265 | color: #DDD !important; 266 | width: calc(100% - 2em) !important; 267 | } 268 | 269 | .from_name { 270 | color: #EEE; 271 | font-weight: bold; 272 | } -------------------------------------------------------------------------------- /site/index.html: -------------------------------------------------------------------------------- 1 | 2 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | Pony Mail! 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 33 | 34 | 35 | 55 | 56 |
57 | 58 | 60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 | 72 |

Welcome to Pony Mail!

73 |

74 | Pick a mailing list domain to start viewing lists: 75 |

76 | 82 |
83 |
84 |
85 | 86 |
87 |
88 | 89 |
90 |
91 | 92 |
 
93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /tools/copy-list.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # Licensed to the Apache Software Foundation (ASF) under one or more 4 | # contributor license agreements. See the NOTICE file distributed with 5 | # this work for additional information regarding copyright ownership. 6 | # The ASF licenses this file to You under the Apache License, Version 2.0 7 | # (the "License"); you may not use this file except in compliance with 8 | # the License. You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | """ Copy lists 19 | 20 | This utility can be used to: 21 | - copy a list within a database 22 | - copy a list to a new database 23 | 24 | """ 25 | 26 | import sys 27 | import time 28 | import argparse 29 | 30 | from elastic import Elastic 31 | 32 | sourceLID = None 33 | targetLID = None 34 | wildcard = None 35 | debug = False 36 | notag = False 37 | newdb = None 38 | 39 | # get config and set up default databas 40 | es = Elastic() 41 | # default database name 42 | dbname = es.getdbname() 43 | 44 | rootURL = "" 45 | 46 | parser = argparse.ArgumentParser(description='Command line options.') 47 | parser.add_argument('--source', dest='source', type=str, required=True, 48 | metavar='', help='Source list to edit') 49 | parser.add_argument('--target', dest='target', type=str, 50 | metavar='', help='(optional) new list ID') 51 | parser.add_argument('--newdb', dest='newdb', type=str, 52 | metavar='', help='(optional) new ES database name') 53 | parser.add_argument('--wildcard', dest='glob', action='store_true', 54 | help='Allow wildcards in --source') 55 | parser.add_argument('--notag', dest='notag', action='store_true', 56 | help='List IDs do not have <> in them') 57 | 58 | args = parser.parse_args() 59 | 60 | sourceLID = args.source 61 | targetLID = args.target 62 | newdb = args.newdb 63 | wildcard = args.glob 64 | notag = args.notag 65 | 66 | if not (targetLID or newdb): 67 | print("Nothing to do! No target list ID or DB name specified") 68 | parser.print_help() 69 | sys.exit(-1) 70 | 71 | sourceLID = ("%s" if notag else "<%s>") % sourceLID.replace("@", ".").strip("<>") 72 | if newdb and not targetLID: 73 | targetLID = sourceLID 74 | 75 | if targetLID: 76 | targetLID = "<%s>" % targetLID.replace("@", ".").strip("<>") 77 | 78 | if targetLID == sourceLID and not newdb: 79 | print("Nothing to do! Target same as source") 80 | parser.print_help() 81 | sys.exit(-1) 82 | 83 | print("Beginning list copy:") 84 | print(" - Source ID: %s" % sourceLID) 85 | if targetLID: 86 | print(" - Target ID: %s" % targetLID) 87 | if newdb: 88 | print(" - Target DB: %s" % newdb) 89 | if not es.indices.exists(newdb): 90 | print("Target database does not exist!") 91 | sys.exit(-1) 92 | 93 | count = 0 94 | 95 | 96 | print("Updating docs...") 97 | then = time.time() 98 | query = { 99 | 'query': { 100 | 'bool': { 101 | 'must': [ 102 | { 103 | 'wildcard' if wildcard else 'term': { 104 | 'list_raw': sourceLID 105 | } 106 | } 107 | ] 108 | } 109 | } 110 | } 111 | js_arr = [] 112 | for page in es.scan_and_scroll(body = query): 113 | sid = page['_scroll_id'] 114 | for hit in page['hits']['hits']: 115 | doc = hit['_id'] 116 | body = es.get(doc_type = 'mbox', id = doc) 117 | srcdoc = doc # save 118 | if targetLID != sourceLID: 119 | doc = hit['_id'].replace(sourceLID,targetLID) 120 | body['_source']['mid'] = doc 121 | body['_source']['list_raw'] = targetLID 122 | body['_source']['list'] = targetLID 123 | js_arr.append({ 124 | '_op_type': 'index', 125 | '_index': newdb if newdb else dbname, 126 | '_type': 'mbox', 127 | '_id': doc, 128 | '_source': body['_source'] 129 | }) 130 | source = es.get(doc_type = 'mbox_source', id = srcdoc, ignore=404) 131 | if source['found']: 132 | js_arr.append({ 133 | '_op_type': 'index', 134 | '_index': newdb if newdb else dbname, 135 | '_type': 'mbox_source', 136 | '_id': doc, 137 | '_source': source['_source'] 138 | }) 139 | else: 140 | print("Source for %s not found, hmm..." % doc) 141 | 142 | count += 1 143 | if (count % 50 == 0): 144 | print("Processed %u emails..." % count) 145 | es.bulk(js_arr) 146 | js_arr = [] 147 | 148 | if len(js_arr) > 0: 149 | es.bulk(js_arr) 150 | 151 | print("All done, processed %u docs in %u seconds" % (count, time.time() - then)) 152 | -------------------------------------------------------------------------------- /site/api/lib/user.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Licensed to the Apache Software Foundation (ASF) under one or more 3 | contributor license agreements. See the NOTICE file distributed with 4 | this work for additional information regarding copyright ownership. 5 | The ASF licenses this file to You under the Apache License, Version 2.0 6 | (the "License"); you may not use this file except in compliance with 7 | the License. You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | ]]-- 17 | 18 | local elastic = require 'lib/elastic' 19 | local config = require 'lib/config' 20 | local JSON = require 'cjson' 21 | 22 | -- allow local override of secure cookie attribute 23 | -- Note: the config item is named to make it more obvious that enabling it is not recommended 24 | -- This makes the expression below a bit more complicated 25 | local SECURE = not(config.allow_insecure_cookie or false) 26 | 27 | -- Get user data from DB 28 | local function getUser(r, override) 29 | local ocookie = r:getcookie("ponymail") 30 | local login = {} 31 | if override or (ocookie and #ocookie > 43) then 32 | local cookie, cid = r:unescape(ocookie or ""):match("([a-f0-9]+)==(.+)") 33 | if override or (cookie and #cookie >= 40 and cid) then 34 | local js = elastic.get('account', r:sha1(override or cid), true) 35 | if js and js.credentials and (override or (cookie == js.internal.cookie)) then 36 | 37 | login = { 38 | credentials = { 39 | email = js.credentials.email, 40 | fullname = js.credentials.fullname, 41 | uid = js.credentials.uid, 42 | altemail = js.credentials.altemail 43 | }, 44 | cid = cid, 45 | internal = { 46 | cookie = cookie, 47 | admin = js.internal.admin, 48 | oauth_used = js.internal.oauth_used, 49 | ip = r.useragent_ip 50 | }, 51 | preferences = js.preferences, 52 | favorites = js.favorites 53 | } 54 | return login 55 | end 56 | end 57 | end 58 | return nil 59 | end 60 | 61 | -- Update or set up a new user 62 | local function updateUser(r, cid, data) 63 | local cookie = r:sha1(r.useragent_ip .. ':' .. (math.random(1,9999999)*os.time()) .. r:clock()) 64 | 65 | -- Does this account exists? If so, grab the prefs first 66 | local prefs = nil 67 | local favs = nil 68 | local oaccount = getUser(r, cid) 69 | if oaccount and oaccount.preferences then 70 | prefs = oaccount.preferences 71 | favs = oaccount.favorites 72 | end 73 | elastic.index(r:sha1(cid), 'account', { 74 | credentials = { 75 | uid = data.uid, 76 | email = data.email, 77 | fullname = data.fullname, 78 | altemail = data.altemail or (oaccount and oaccount.credentials.altemail) or {} 79 | }, 80 | internal = { 81 | admin = data.admin, 82 | cookie = cookie, 83 | oauth_used = data.oauth_used, 84 | ip = r.useragent_ip 85 | }, 86 | cid = cid, 87 | preferences = prefs, 88 | favorites = favs 89 | }) 90 | r:setcookie{ 91 | key = "ponymail", 92 | value = cookie .. "==" .. (cid), 93 | secure = SECURE, 94 | httponly = true, 95 | path = "/" 96 | } 97 | end 98 | 99 | 100 | -- Log out a user 101 | local function logoutUser(r, usr) 102 | if usr and usr.cid then 103 | local js = elastic.get('account', r:sha1(usr.cid)) 104 | js.internal.cookie = 'nil' 105 | elastic.index(r:sha1(usr.cid), 'account', js) 106 | end 107 | r:setcookie{ 108 | key = "ponymail", 109 | value = "-----", 110 | path = "/" 111 | } 112 | end 113 | 114 | 115 | -- Save preferences 116 | local function savePreferences(r, usr, alts) 117 | if usr and usr.cid then 118 | local js = elastic.get('account', r:sha1(usr.cid)) 119 | js.preferences = usr.preferences 120 | if alts then 121 | js.credentials.altemail = usr.credentials.altemail 122 | end 123 | elastic.index(r:sha1(usr.cid), 'account', js) 124 | end 125 | end 126 | 127 | -- Save favorites 128 | local function saveFavorites(r, usr) 129 | if usr and usr.cid then 130 | local js = elastic.get('account', r:sha1(usr.cid)) 131 | js.favorites = usr.favorites 132 | elastic.index(r:sha1(usr.cid), 'account', js) 133 | end 134 | end 135 | 136 | return { 137 | get = getUser, 138 | logout = logoutUser, 139 | save = savePreferences, 140 | update = updateUser, 141 | favs = saveFavorites 142 | } -------------------------------------------------------------------------------- /site/thread.html: -------------------------------------------------------------------------------- 1 | 2 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | Pony Mail! 21 | 22 | 23 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 37 | 38 | 39 | 65 | 66 | 67 |
68 | 69 | 71 |
72 | 73 | 74 |
75 |
76 | 77 | 78 | 79 | 81 | 82 | 83 | 84 | 85 |
 
86 | 93 | 94 | -------------------------------------------------------------------------------- /site/js/dev/ponymail_zzz.js: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one or more 3 | contributor license agreements. See the NOTICE file distributed with 4 | this work for additional information regarding copyright ownership. 5 | The ASF licenses this file to You under the Apache License, Version 2.0 6 | (the "License"); you may not use this file except in compliance with 7 | the License. You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | 18 | 19 | // dealWithKeyboard: Handles what happens when you hit the escape key 20 | function dealWithKeyboard(e) { 21 | 22 | // escape key: hide composer/settings/thread 23 | if (e.keyCode == 27) { 24 | if (document.getElementById('splash').style.display == 'block') { 25 | document.getElementById('splash').style.display = "none" 26 | saveDraft() 27 | } else if (location.href.search(/list\.html/) != -1) { // should only work for the list view 28 | 29 | // If datepicker popup is shown, hide it on escape 30 | var thread = document.getElementById('thread_' + current_thread.toString().replace(/@<.+>/, "")) 31 | // try treeview if all else fails 32 | if (!thread) { 33 | thread = document.getElementById('thread_treeview_' + current_thread.toString().replace(/@<.+>/, "")) 34 | } 35 | if (document.getElementById('datepicker_popup') && document.getElementById('datepicker_popup').style.display == "block") { 36 | document.getElementById('datepicker_popup').style.display = "none" 37 | } 38 | // otherwise, collapse a thread? 39 | else if (thread) { 40 | if (thread.style.display == 'block') { 41 | if (prefs.displayMode == 'treeview') { 42 | toggleEmails_threaded(current_thread, true) 43 | toggleEmails_treeview(current_thread, true) 44 | } else { 45 | toggleEmails_threaded(current_thread, true) 46 | } 47 | } else { 48 | // Close all threads? 49 | kiddos = [] 50 | traverseThread(document.body, '(thread|helper)_', 'DIV') 51 | for (var i in kiddos) { 52 | kiddos[i].style.display = 'none'; 53 | } 54 | } 55 | } 56 | } 57 | } 58 | 59 | // Make sure the below shortcuts don't interfere with normal operations 60 | if (document.getElementById('splash').style.display != 'block' && document.activeElement.nodeName != 'INPUT' && !e.ctrlKey) { 61 | 62 | // H key: show help 63 | if (e.keyCode == 72) { 64 | popup("Keyboard shortcuts", 65 | "
\
 66 |                   H:Show this help menu
\ 67 | C:Compose a new email to the current list
\ 68 | R:Reply to the last opened email
\ 69 | S:Go to the quick search bar
\ 70 | Esc:Hide/collapse current email or thread
\ 71 |
\ 72 | You can also, in some cases, use the mouse wheel to scroll up/down the list view", 73 | 10 74 | ) 75 | } 76 | 77 | // C key: compose 78 | else if (e.keyCode == 67) { 79 | compose(null, xlist, 'new') 80 | } 81 | // R key: reply 82 | else if (e.keyCode == 82) { 83 | if (openEmail() && last_opened_email) { 84 | compose(last_opened_email, null, 'reply') 85 | } 86 | } 87 | // S key: quick search 88 | else if (e.keyCode == 83) { 89 | if (document.getElementById('q')) { 90 | document.getElementById('q').focus() 91 | } 92 | 93 | } 94 | } 95 | } 96 | 97 | 98 | // Add Pony Mail powered-by footer 99 | var footer = document.createElement('footer') 100 | footer.setAttribute("class", 'footer') 101 | footer.style.height = "32px" 102 | footer.style.width = "100%" 103 | var fd = document.createElement('div') 104 | fd.setAttribute("class", "container") 105 | fd.innerHTML = "

Powered by Apache Pony Mail (Incubating) v/" + _VERSION_ + ".

" 106 | footer.appendChild(fd) 107 | document.body.appendChild(footer) 108 | 109 | // Add listener for keys (mostly for escape key for hiding stuff) 110 | window.addEventListener("keyup", dealWithKeyboard, false); 111 | 112 | // Add listener for when URLs get popped from the browser history 113 | window.onpopstate = function(event) { 114 | getListInfo(null, document.location.search.substr(1), true) 115 | } 116 | -------------------------------------------------------------------------------- /tools/missing.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # Licensed to the Apache Software Foundation (ASF) under one or more 4 | # contributor license agreements. See the NOTICE file distributed with 5 | # this work for additional information regarding copyright ownership. 6 | # The ASF licenses this file to You under the Apache License, Version 2.0 7 | # (the "License"); you may not use this file except in compliance with 8 | # the License. You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | """ Scan messages to find and optionally fix missing fields 19 | 20 | """ 21 | 22 | import argparse 23 | import time 24 | from elastic import Elastic 25 | 26 | parser = argparse.ArgumentParser(description='Command line options.') 27 | # Cannot have both source and mid as input 28 | source_group = parser.add_mutually_exclusive_group(required=True) 29 | source_group.add_argument('--source', dest='source', type=str, metavar='list-name', 30 | help='Source list to edit') 31 | source_group.add_argument('--mid', dest='mid', type=str, metavar='message-id', 32 | help='Source Message-ID to edit') 33 | 34 | action_group = parser.add_mutually_exclusive_group(required=True) 35 | # N.B. Use nargs=1 below, because the same field is used for get and set 36 | action_group.add_argument('--listmissing', dest='missing', type=str, nargs=1, metavar='fieldname', 37 | help='list missing fields') 38 | action_group.add_argument('--setmissing', dest='missing', type=str, nargs=2, metavar=('fieldname', 'value'), 39 | help='set missing fields') 40 | 41 | # Generic arguments 42 | parser.add_argument('--notag', dest='notag', action='store_true', 43 | help='List IDs do not have <> in them') 44 | parser.add_argument('--wildcard', dest='wildcard', action='store_true', 45 | help='Allow wildcards in --source') 46 | parser.add_argument('--debug', dest='debug', action='store_true', 47 | help='Debug output - very noisy!') 48 | parser.add_argument('--test', dest='test', action='store_true', 49 | help='Only test for occurrences, do not run the chosen action (dry run)') 50 | 51 | args = parser.parse_args() 52 | 53 | if args.wildcard and args.mid: 54 | parser.error("Cannot use --mid and --wildcard together") 55 | 56 | def getField(src,name): 57 | try: 58 | return src[name] 59 | except KeyError: 60 | return '(Uknown)' 61 | 62 | def update(es, arr): 63 | if args.debug: 64 | print(arr) 65 | if not args.test: 66 | es.bulk(arr) 67 | 68 | setField = len(args.missing) > 1 69 | field = args.missing[0] 70 | value = None 71 | if setField: 72 | value = args.missing[1] 73 | print("Set missing/null field %s to '%s'" %(field, value)) 74 | else: 75 | print("List missing/null field %s" % field) 76 | count = 0 77 | then = time.time() 78 | elastic = Elastic() 79 | if args.source: 80 | sourceLID = ("%s" if args.notag else "<%s>") % args.source.replace("@", ".").strip("<>") 81 | query = { 82 | "_source" : ['subject','message-id'], 83 | "query" : { 84 | "bool" : { 85 | "must" : { 86 | 'wildcard' if args.wildcard else 'term': { 87 | 'list_raw': sourceLID 88 | } 89 | }, 90 | # missing is not supported in ES 5.x 91 | "must_not": { 92 | "exists" : { 93 | "field" : field 94 | } 95 | } 96 | } 97 | } 98 | } 99 | js_arr = [] 100 | for page in elastic.scan_and_scroll(body = query): 101 | if args.debug: 102 | print(page) 103 | for hit in page['hits']['hits']: 104 | doc = hit['_id'] 105 | body = {} 106 | if setField: 107 | body[field] = value 108 | js_arr.append({ 109 | '_op_type': 'update', 110 | '_index': elastic.dbname, 111 | '_type': 'mbox', 112 | '_id': doc, 113 | 'doc': body 114 | }) 115 | count += 1 116 | source = hit['_source'] 117 | print("Id: %s Msg-id: %s Subject: %s" %(doc, getField(source, 'message-id'), getField(source,'subject'))) 118 | if (count % 500 == 0): 119 | print("Processed %u emails..." % count) 120 | if setField: 121 | update(elastic, js_arr) 122 | js_arr = [] 123 | 124 | print("Processed %u emails." % count) 125 | if len(js_arr) > 0: 126 | if setField: 127 | update(elastic, js_arr) 128 | 129 | if args.mid: 130 | parser.error("--mid: not yet implemented") 131 | 132 | print("All done, processed %u docs in %u seconds" % (count, time.time() - then)) 133 | -------------------------------------------------------------------------------- /site/api/email.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Licensed to the Apache Software Foundation (ASF) under one or more 3 | contributor license agreements. See the NOTICE file distributed with 4 | this work for additional information regarding copyright ownership. 5 | The ASF licenses this file to You under the Apache License, Version 2.0 6 | (the "License"); you may not use this file except in compliance with 7 | the License. You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | ]]-- 17 | 18 | -- This is email.lua - a script for fetching a document (email) 19 | 20 | local JSON = require 'cjson' 21 | local elastic = require 'lib/elastic' 22 | local aaa = require 'lib/aaa' 23 | local user = require 'lib/user' 24 | local cross = require 'lib/cross' 25 | local config = require 'lib/config' 26 | local utils = require 'lib/utils' 27 | local mime = require "mime" 28 | 29 | function handle(r) 30 | cross.contentType(r, "application/json; charset=UTF-8") 31 | local get = r:parseargs() 32 | -- get the parameter (if any) and tidy it up 33 | local eid = (get.id or ""):gsub('"', '%%22') 34 | -- If it is the empty string then set it to "1" so ES doesn't barf 35 | -- N.B. ?id is treated as ?id=1 36 | if #eid == 0 then eid = "1" end 37 | local doc = elastic.get("mbox", eid, true) 38 | 39 | -- Try searching by original source mid if not found, for backward compat 40 | if not doc or not doc.mid then 41 | doc = nil -- ensure subsequent check works if we don't find the email here either 42 | local docs = elastic.find("message-id:\"" .. r:escape(eid) .. "\"", 1, "mbox") 43 | if #docs == 1 then 44 | doc = docs[1] 45 | end 46 | 47 | -- shortened link maybe? 48 | if #docs == 0 and #eid == utils.SHORTENED_LINK_LEN then 49 | docs = elastic.find("mid:" .. r:escape(eid) .. "*", 1, "mbox") 50 | end 51 | if #docs == 1 then 52 | doc = docs[1] 53 | end 54 | end 55 | 56 | -- Did we find an email? 57 | if doc then 58 | local account = user.get(r) 59 | 60 | -- If we can access this email, ... 61 | if aaa.canAccessDoc(r, doc, account) then 62 | -- Because we allow quotes in message-IDs, we need to escape for standard UI. 63 | doc.tid = doc.request_id:gsub('"', '%%22') 64 | doc.mid = doc.mid:gsub('"', '%%22') 65 | 66 | -- Are we in fact looking for an attachment inside this email? 67 | if get.attachment then 68 | local hash = r:escape(get.file) 69 | local fdoc = elastic.get("attachment", hash) 70 | if fdoc and fdoc.source then 71 | local out = mime.unb64(fdoc.source) 72 | local ct = "application/binary" 73 | local fn = "unknown" 74 | local fs = 0 75 | for k, v in pairs(doc.attachments or {}) do 76 | if v.hash == hash then 77 | ct = v.content_type or "application/binary" 78 | fn = v.filename 79 | fs = v.size 80 | break 81 | end 82 | end 83 | cross.contentType(r, ct) 84 | r.headers_out['Content-Length'] = fs 85 | if not (ct:match("image") or ct:match("text")) then 86 | r.headers_out['Content-Disposition'] = ("attachment; filename=\"%s\";"):format(fn) 87 | end 88 | r:write(out) 89 | return cross.OK 90 | end 91 | -- Or do we just want the email itself? 92 | else 93 | local eml = utils.extractCanonEmail(doc.from or "unknown") 94 | 95 | -- Anonymize to/cc if full_headers is false 96 | -- do this before anonymizing the headers 97 | if not config.full_headers or not account then 98 | doc.to = nil 99 | doc.cc = nil 100 | end 101 | 102 | if not account then -- anonymize email address if not logged in 103 | doc = utils.anonymizeHdrs(doc, true) 104 | end 105 | 106 | -- Anonymize any email address mentioned in the email if not logged in 107 | if not account and config.antispam then 108 | doc.body = utils.anonymizeBody(doc.body) 109 | end 110 | 111 | 112 | doc.gravatar = r:md5(eml:lower()) 113 | r:puts(JSON.encode(doc)) 114 | return cross.OK 115 | end 116 | end 117 | end 118 | r:puts(JSON.encode{error = "No such e-mail or you do not have access to it."}) 119 | return cross.OK 120 | end 121 | 122 | cross.start(handle) 123 | -------------------------------------------------------------------------------- /site/css/metro.css: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one or more 3 | contributor license agreements. See the NOTICE file distributed with 4 | this work for additional information regarding copyright ownership. 5 | The ASF licenses this file to You under the Apache License, Version 2.0 6 | (the "License"); you may not use this file except in compliance with 7 | the License. You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | html { 18 | font-family: sans-serif; 19 | } 20 | 21 | body { 22 | margin: 0; 23 | } 24 | 25 | .datepicker { 26 | background-color: #480032 !important; 27 | } 28 | 29 | a:active, a:focus { 30 | color: #268bd2; 31 | } 32 | 33 | a:active, 34 | a:hover { 35 | outline: 0; 36 | } 37 | 38 | h1 { 39 | font-size: 2em; 40 | } 41 | 42 | code, 43 | kbd, 44 | pre { 45 | font-family: monospace, serif; 46 | font-size: 1em; 47 | } 48 | 49 | pre { 50 | white-space: pre-wrap; 51 | word-wrap: break-word; 52 | } 53 | 54 | q { 55 | quotes: "\201C" "\201D" "\2018" "\2019"; 56 | } 57 | 58 | small { 59 | font-size: 80%; 60 | } 61 | 62 | sub, 63 | sup { 64 | font-size: 75%; 65 | line-height: 0; 66 | position: relative; 67 | vertical-align: baseline; 68 | } 69 | 70 | fieldset { 71 | border: 1px solid #c0c0c0; 72 | margin: 0 2px; 73 | padding: 0.35em 0.625em 0.75em; 74 | } 75 | 76 | legend { 77 | border: 0; 78 | padding: 0; 79 | } 80 | 81 | button, 82 | input, 83 | select, 84 | textarea { 85 | font-family: inherit; 86 | font-size: 100%; 87 | margin: 0; 88 | } 89 | 90 | button, 91 | input { 92 | line-height: normal; 93 | } 94 | 95 | button, 96 | 97 | input[type="checkbox"], 98 | input[type="radio"] { 99 | box-sizing: border-box; 100 | padding: 0; 101 | } 102 | 103 | button::-moz-focus-inner, 104 | input::-moz-focus-inner { 105 | border: 0; 106 | padding: 0; 107 | } 108 | 109 | textarea { 110 | overflow: auto; 111 | vertical-align: top; 112 | } 113 | 114 | table { 115 | border-collapse: collapse; 116 | border-spacing: 0; 117 | } 118 | 119 | html { 120 | font-family: sans-serif; 121 | } 122 | 123 | pre, 124 | code { 125 | font-family: sans-serif; 126 | } 127 | 128 | h1, 129 | h2, 130 | h3, 131 | h4, 132 | h5, 133 | h6 { 134 | font-family: sans-serif; 135 | font-weight: 700; 136 | } 137 | 138 | html { 139 | background-color: #282828; 140 | color: #9C9C9C; 141 | } 142 | 143 | body { 144 | background-color: #2E2E2E; 145 | border: 1pt solid #586e75; 146 | } 147 | 148 | .well { 149 | background-color: #041E55; 150 | color: #EEE; 151 | } 152 | 153 | .well li { 154 | background-color: #021A4D; 155 | } 156 | 157 | .well li:hover { 158 | background-color: #071E51; 159 | } 160 | 161 | .btn-danger { 162 | background-color: #610043 !important; 163 | color: #EEE; 164 | } 165 | 166 | .btn-success { 167 | background-color: #597900 !important; 168 | color: #EEE; 169 | } 170 | 171 | .btn-primary { 172 | background-color: #804D00 !important; 173 | color: #EEE; 174 | } 175 | 176 | .btn-warning { 177 | background-color: #071E51 !important; 178 | border-color: #082E61 !important; 179 | color: #EEE; 180 | } 181 | 182 | .bubble-info, .bubble-warning, .bubble-danger, .bubble-success, .bubble-primary, .bubble-topic { 183 | background: #2D2C2B !important; 184 | color: #e5b900; 185 | } 186 | 187 | .reply { 188 | background-color: #000E2C; 189 | border: 1pt solid #586e75; 190 | box-shadow: 5pt 5pt 8pt #073642; 191 | color: #DDD; 192 | padding: 1em; 193 | } 194 | 195 | #stats { 196 | background-color: #425B00; 197 | margin: 10px; 198 | border-radius: 5px; 199 | border: 1pt solid #597900; 200 | color: #EEE; 201 | } 202 | 203 | code { 204 | background-color: #073642; 205 | padding: 2px; 206 | } 207 | 208 | a { 209 | color: #FFF; 210 | } 211 | 212 | a:visited { 213 | color: #FFC; 214 | } 215 | 216 | a:hover { 217 | color: #FFB; 218 | } 219 | 220 | h1 { 221 | color: #d33682; 222 | } 223 | 224 | h2, 225 | h3, 226 | h4, 227 | h5, 228 | h6 { 229 | color: #EEE; 230 | } 231 | 232 | pre { 233 | background-color: #002b36; 234 | color: #839496; 235 | border: 1pt solid #586e75; 236 | padding: 1em; 237 | box-shadow: 5pt 5pt 8pt #073642; 238 | } 239 | 240 | pre code { 241 | background-color: #002b36; 242 | } 243 | 244 | h1 { 245 | font-size: 2.4em; 246 | } 247 | 248 | h2 { 249 | font-size: 2.0em; 250 | } 251 | 252 | h3 { 253 | font-size: 1.4em; 254 | } 255 | 256 | h4 { 257 | font-size: 1.2em; 258 | } 259 | 260 | .navbar { 261 | background: #480032 !important; 262 | color: #DDD !important; 263 | } 264 | 265 | .active a { 266 | background: #58003D !important; 267 | color: #EEE !important; 268 | } 269 | 270 | .nav a:hover { 271 | color: #EEE !important; 272 | } 273 | 274 | .nav .open a { 275 | background: #58003D !important; 276 | color: #EEE !important; 277 | } 278 | 279 | .nav .open a:hover { 280 | background: #610043 !important; 281 | color: #FFF !important; 282 | font-weight: bold; 283 | } 284 | 285 | .footer { 286 | background: #58003D !important; 287 | color: #DDD !important; 288 | width: calc(100% - 2em) !important; 289 | } 290 | 291 | .from_name { 292 | color: #FFF !important; 293 | font-weight: bold; 294 | } --------------------------------------------------------------------------------