Merge pull request #251 from raboof/show-oauth-error

Show error alert when oauth fails
diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml
index 1062ebd..8ea7174 100644
--- a/.github/workflows/integration-tests.yml
+++ b/.github/workflows/integration-tests.yml
@@ -52,7 +52,8 @@
     - name: Setup python
       uses: actions/setup-python@v2
       with:
-        python-version: '3.9'
+        # 3.12 current as of 2024
+        python-version: '3.12'
         architecture: x64
     - name: Install dependencies
       run: |
@@ -60,6 +61,7 @@
         pip install -r tools/requirements.txt
         pip install -r server/requirements.txt
         pip install -r test/requirements.txt
+        pip list
     - name: Basic test
       run: |
         curl -sq "http://localhost:9200/_cluster/health?level=indices&pretty"
diff --git a/.github/workflows/type-tests.yml b/.github/workflows/type-tests.yml
index 16cab27..41632bd 100644
--- a/.github/workflows/type-tests.yml
+++ b/.github/workflows/type-tests.yml
@@ -4,6 +4,7 @@
   push:
     paths-ignore:
       - '**/integration-tests.yml'
+      - '**/unittest.yml'
       - 'test/itest*'
   
   workflow_dispatch:
@@ -15,7 +16,8 @@
     strategy:
       max-parallel: 1
       matrix:
-        python-version: ["3.10", 3.7, 3.9]
+        # 3.8 EOL 2024-10 approx
+        python-version: [3.8, "3.10", 3.12]
     steps:
     - uses: actions/checkout@master
       with:
diff --git a/.github/workflows/unittest.yml b/.github/workflows/unittest.yml
index 077a395..43e66cf 100644
--- a/.github/workflows/unittest.yml
+++ b/.github/workflows/unittest.yml
@@ -13,10 +13,10 @@
 
     runs-on: ubuntu-latest
     strategy:
-      max-parallel: 4
+      max-parallel: 1
       matrix:
-#         python-version: [2.7, 3.5, 3.6, 3.7]
-        python-version: [3.7, "3.10"]
+        # 3.8 EOL 2024-10 approx
+        python-version: [3.8, "3.10", 3.12]
 
     steps:
     - uses: actions/checkout@master
@@ -37,7 +37,10 @@
         python -m pip install --upgrade pip
         pip install -r tools/requirements.txt
         pip install -r test/requirements.txt
-        pip install html2text # optional dependency, but needed for tests
+        # Later versions of html2text cause html-based tests to fail, because of a changed conversion
+        # This only affects the appearance of the message body, so does not matter for compatibility
+        pip install html2text==2020.1.16 # optional dependency, but needed for tests
+        pip list
     - name: Check versions
       run: |
         webui/js/source/build.sh
diff --git a/server/plugins/messages.py b/server/plugins/messages.py
index 584df4d..7443954 100644
--- a/server/plugins/messages.py
+++ b/server/plugins/messages.py
@@ -558,6 +558,7 @@
         """Turns a flat array of emails into a nested structure of threads"""
         for cur_email in sorted(self.emails, key=lambda x: x["epoch"]):
             author = cur_email.get("from")
+            assert(author)
             if author not in self.authors:
                 self.authors[author] = [0, cur_email.get("gravatar", "")]
             self.authors[author][0] += 1
diff --git a/server/requirements.txt b/server/requirements.txt
index a9ad6f3..7642068 100644
--- a/server/requirements.txt
+++ b/server/requirements.txt
@@ -1,5 +1,5 @@
 # Items in this file must have a licence compatible with AL 2.0
-PyYAML~=5.4.1                    # WTFPL
+PyYAML~=6.0.1                    # MIT
 types-PyYAML                     # AL2.0
 multipart~=0.2.1                 # MIT
 elasticsearch-dsl>=7.0.0,<8.0.0  # AL2.0
diff --git a/server/server_version.py b/server/server_version.py
index ad04347..a78d1f6 100644
--- a/server/server_version.py
+++ b/server/server_version.py
@@ -1,2 +1,2 @@
 # This file is generated by server/update_version.sh
-PONYMAIL_SERVER_VERSION = '218c4f5'
+PONYMAIL_SERVER_VERSION = '4d644b1'
diff --git a/test/esintercept/elasticsearch.py b/test/esintercept/elasticsearch.py
new file mode 100644
index 0000000..4e43a70
--- /dev/null
+++ b/test/esintercept/elasticsearch.py
@@ -0,0 +1,83 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+# Very simple module to intercept calls to Elasticsearch methods
+# Writes call parameters to a file.
+#
+# Use it by defining PYTHONPATH to include its parent dir, e.g.
+# [OUT=/tmp/pmfoal_out.txt] PYTHONPATH=test/esintercept python3 tools/archiver.py <file.eml
+# Redefine OUT to change the output file name
+
+import os
+import json
+
+# Dummy to satisf code
+VERSION = (7, 0, 1)
+VERSION_STR = "7.0.1"
+
+import atexit
+
+def exit_handler():
+    print("Closing %s" % outfile)
+    OUT.close()
+
+atexit.register(exit_handler)
+
+outfile=os.getenv('OUT','/tmp/pmfoal_out.txt')
+print("Opening %s" % outfile)
+OUT = open(outfile, 'w', encoding='utf-8')
+
+
+def show(method, *args, **kwargs):
+    OUT.write("======= Method: %s =======\n" % method)
+    bits = {
+        'args': args,
+        'kwargs': kwargs,
+    }
+    json.dump(bits, OUT, indent=2, sort_keys=True)
+    OUT.write("\n")
+
+class helpers:
+    def bulk(self,*args,**kwargs):
+        show('bulk', *args, **kwargs)
+
+class Indices:
+    def exists(self, *args, **kwargs):
+        show('exists', *args, **kwargs)
+        return True # Dummy value
+
+class Elasticsearch:
+
+    indices = Indices()
+
+    def __init__(self, *args, **kwargs):
+        show('Elasticsearch', *args,**kwargs)
+
+    def index(self, *args, **kwargs):
+        show('index', *args, **kwargs)
+
+    def info(self, *args, **kwargs):
+        show('info', *args, **kwargs)
+        return {"version": {"number": VERSION_STR}} # sufficient for testing
+
+class ConnectionError:
+    pass
+
+class AsyncElasticsearch(Elasticsearch):
+    pass
diff --git a/tools/archiver.py b/tools/archiver.py
index 2feb887..9d364fd 100755
--- a/tools/archiver.py
+++ b/tools/archiver.py
@@ -158,6 +158,7 @@
             # Allow for empty string
             if fd is None:
                 return None, None
+            assert(isinstance(fd, bytes)) # decode=True generates bytes
             if filename:
                 attachment = {
                     "content_type": part.get_content_type(),
@@ -202,6 +203,7 @@
         self.bytes = part.get_payload(decode=True)
         self.html_as_source = False
         if self.bytes is not None:
+            assert(isinstance(self.bytes, bytes)) # decode=True generates bytes
             valid_encodings = [x for x in self.charsets if x]
             if valid_encodings:
                 for cs in valid_encodings:
@@ -428,7 +430,11 @@
                 ):
                     first_html = Body(part)
             except Exception as err:
-                print(err)
+                entry = sys.exc_info()[-1]
+                if entry: # avoid mypy complaint
+                    print('Error on line {}:'.format(entry.tb_lineno), type(err).__name__, err)
+                else: # Should not happen, but just in case
+                    print('Failed to create Body(part):',type(err).__name__, err)
 
         # this requires a GPL lib, user will have to install it themselves
         if first_html and (
@@ -759,7 +765,7 @@
         # If we have a dump dir and ES failed, push to dump dir instead as a JSON object
         # We'll leave it to another process to pick up the slack.
         except Exception as err:
-            print(err)
+            print('Error on line {}:'.format(sys.exc_info()[-1].tb_lineno), type(err).__name__, err)
             if dump:
                 print(
                     "Pushing to ES failed, but dumponfail specified, dumping JSON docs"
diff --git a/tools/requirements.txt b/tools/requirements.txt
index 7a70f22..9aedca9 100644
--- a/tools/requirements.txt
+++ b/tools/requirements.txt
@@ -1,5 +1,5 @@
 # Items in this file must have a licence compatible with AL 2.0
-PyYAML~=5.4.1                     # WTFPL
+PyYAML~=6.0.1                     # MIT
 # elasticsearch-dsl>=7.0.0,<8.0.0   # AL2.0 - not used by tools currently
 # N.B. ES 7.14 introduces strict server version compatibility checks
 elasticsearch[async]>=7.13.1,<7.14.0  # AL2.0
diff --git a/webui/admin.html b/webui/admin.html
index 6473d30..53a2b1c 100644
--- a/webui/admin.html
+++ b/webui/admin.html
@@ -25,7 +25,7 @@
     <!-- Bootstrap -->
 
     <link href="css/bootstrap.min.css" rel="stylesheet" media="all">
-    <link href="css/scaffolding.css?revision=f3a4716" rel="stylesheet" media="all">
+    <link href="css/scaffolding.css?revision=f8dc8ff" rel="stylesheet" media="all">
     <link href="css/modal.css" rel="stylesheet" media="all">
     <link href="css/spinner.css" rel="stylesheet" media="all">
     <link rel="alternate" href="/api/static.lua"/>
@@ -79,9 +79,9 @@
     <script src="js/jquery-1.12.4.min.js" integrity="sha256-ZosEbRLbNQzLpnKIkEdrPv7lOy9C27hHQ+Xp8a4MxAQ="></script>
     <!-- Include all compiled plugins (below), or include individual files as needed -->
     <script src="js/bootstrap.min.js"></script>
-    <script src="js/config.js?revision=f3a4716"></script>
-    <script src="js/wordcloud.js?revision=f3a4716"></script>
-    <script src="js/ponymail.js?revision=f3a4716"></script>
+    <script src="js/config.js?revision=f8dc8ff"></script>
+    <script src="js/wordcloud.js?revision=f8dc8ff"></script>
+    <script src="js/ponymail.js?revision=f8dc8ff"></script>
     <div id="splash" class="splash fade-in"> &nbsp; </div>
     <div style="clear: both;"></div>
   </body>
diff --git a/webui/index.html b/webui/index.html
index ddcf7a4..b803470 100644
--- a/webui/index.html
+++ b/webui/index.html
@@ -24,7 +24,7 @@
     <!-- Bootstrap -->
     
     <link href="css/bootstrap.min.css" rel="stylesheet" media="all">
-    <link href="css/scaffolding.css?revision=f3a4716" rel="stylesheet" media="all">
+    <link href="css/scaffolding.css?revision=f8dc8ff" rel="stylesheet" media="all">
     <link href="css/modal.css" rel="stylesheet" media="all">
     <link href="css/spinner.css" rel="stylesheet" media="all">
     <link rel="alternate" href="/api/static.lua"/>
@@ -63,8 +63,8 @@
     <script src="js/jquery-1.12.4.min.js" integrity="sha256-ZosEbRLbNQzLpnKIkEdrPv7lOy9C27hHQ+Xp8a4MxAQ="></script>
     <!-- Include all compiled plugins (below), or include individual files as needed -->
     <script src="js/bootstrap.min.js"></script>
-    <script src="js/config.js?revision=f3a4716"></script>
-    <script src="js/ponymail.js?revision=f3a4716"></script>
+    <script src="js/config.js?revision=f8dc8ff"></script>
+    <script src="js/ponymail.js?revision=f8dc8ff"></script>
     <div id="splash" class="splash fade-in"> &nbsp; </div>
     <div style="clear: both;"></div>
     
diff --git a/webui/js/oauth.js b/webui/js/oauth.js
index 63ca3da..e855b40 100644
--- a/webui/js/oauth.js
+++ b/webui/js/oauth.js
@@ -30,18 +30,21 @@
     xmlHttp.open("GET", theUrl, true);
     xmlHttp.send(null);
     xmlHttp.onreadystatechange = function(state) {
-        if (xmlHttp.readyState == 4 && xmlHttp.status == 200) {
-            if (callback) {
-                try {
-                    callback(JSON.parse(xmlHttp.responseText), xstate);
-                } catch (e) {
-                    callback(JSON.parse(xmlHttp.responseText), xstate)
+        if (xmlHttp.readyState == 4) {
+            if (xmlHttp.status == 200) {
+                if (callback) {
+                    try {
+                        callback(JSON.parse(xmlHttp.responseText), xstate);
+                    } catch (e) {
+                        callback(JSON.parse(xmlHttp.responseText), xstate)
+                    }
                 }
-            }
 
-        }
-        if (xmlHttp.readyState == 4 && xmlHttp.status == 404) {
-            alert("404'ed: " + theUrl)
+            } else if (xmlHttp.status == 404) {
+                alert("404'ed: " + theUrl)
+            } else if (xmlHttp.status >= 500) {
+                alert("Internal error fetching " + theUrl + ": " + xmlHttp.responseText)
+            }
         }
     }
 }
diff --git a/webui/js/ponymail.js b/webui/js/ponymail.js
index 6b6a35e..6f990fc 100644
--- a/webui/js/ponymail.js
+++ b/webui/js/ponymail.js
@@ -16,7 +16,7 @@
 */
 // THIS IS AN AUTOMATICALLY COMBINED FILE. PLEASE EDIT THE source/ FILES!
 
-const PONYMAIL_REVISION = 'f3a4716';
+const PONYMAIL_REVISION = 'f8dc8ff';
 
 
 /******************************************
@@ -627,7 +627,7 @@
             quote = m[0];
             i = quote.length;
             t = splicer.substr(0, i);
-            quote = quote.replace(/(>*\s*\r?\n)+$/g, "");
+            quote = quote.replace(/\n>[>\s]*$/g, "\n");
             qdiv = new HTML('div', {
                 "class": "email_quote_parent"
             }, [
@@ -694,8 +694,18 @@
     request.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
     request.send(content.join("&")); // send email as a POST string
 
-    document.getElementById('composer_modal').style.display = 'none';
-    modal("Message dispatched!", "Your email has been sent. Depending on moderation rules, it may take a while before it shows up in the archives.", "help");
+    request.onreadystatechange = function(state) {
+        if (request.readyState == 4) {
+            document.getElementById('composer_modal').style.display = 'none';
+            let response = JSON.parse(request.responseText)
+            if (response.error) {
+                modal("Message dispatch failed!", response.error, "error");
+            } else {
+                modal("Message dispatched!", "Your email has been sent. Depending on moderation rules, it may take a while before it shows up in the archives.", "help");
+            }
+        }
+    }
+
 }
 
 function compose_email(replyto, list) {
diff --git a/webui/js/source/composer.js b/webui/js/source/composer.js
index 7aabb8f..08fe5be 100644
--- a/webui/js/source/composer.js
+++ b/webui/js/source/composer.js
@@ -17,8 +17,18 @@
     request.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
     request.send(content.join("&")); // send email as a POST string
 
-    document.getElementById('composer_modal').style.display = 'none';
-    modal("Message dispatched!", "Your email has been sent. Depending on moderation rules, it may take a while before it shows up in the archives.", "help");
+    request.onreadystatechange = function(state) {
+        if (request.readyState == 4) {
+            document.getElementById('composer_modal').style.display = 'none';
+            let response = JSON.parse(request.responseText)
+            if (response.error) {
+                modal("Message dispatch failed!", response.error, "error");
+            } else {
+                modal("Message dispatched!", "Your email has been sent. Depending on moderation rules, it may take a while before it shows up in the archives.", "help");
+            }
+        }
+    }
+
 }
 
 function compose_email(replyto, list) {
diff --git a/webui/list.html b/webui/list.html
index 4006aee..87aa518 100644
--- a/webui/list.html
+++ b/webui/list.html
@@ -24,7 +24,7 @@
     <!-- Bootstrap -->
     
     <link href="css/bootstrap.min.css" rel="stylesheet" media="all">
-    <link href="css/scaffolding.css?revision=f3a4716" rel="stylesheet" media="all">
+    <link href="css/scaffolding.css?revision=f8dc8ff" rel="stylesheet" media="all">
     <link href="css/modal.css" rel="stylesheet" media="all">
     <link href="css/spinner.css" rel="stylesheet" media="all">
     <link rel="alternate" href="/api/static.lua"/>
@@ -181,9 +181,9 @@
     </script>
     <!-- Include all compiled plugins (below), or include individual files as needed -->
     <script src="js/bootstrap.min.js"></script>
-    <script src="js/config.js?revision=f3a4716"></script>
-    <script src="js/wordcloud.js?revision=f3a4716"></script>
-    <script src="js/ponymail.js?revision=f3a4716"></script>
+    <script src="js/config.js?revision=f8dc8ff"></script>
+    <script src="js/wordcloud.js?revision=f8dc8ff"></script>
+    <script src="js/ponymail.js?revision=f8dc8ff"></script>
     <div id="splash" class="splash fade-in"> &nbsp; </div>
     <div style="clear: both;"></div>
     <script type="text/javascript">
diff --git a/webui/oauth.html b/webui/oauth.html
index f44c0f8..833682c 100644
--- a/webui/oauth.html
+++ b/webui/oauth.html
@@ -21,7 +21,7 @@
 
     <!-- CSS -->
     <link href="css/bootstrap.min.css" rel="stylesheet" media="all">
-    <link href="css/scaffolding.css?revision=f3a4716" rel="stylesheet" media="all">
+    <link href="css/scaffolding.css?revision=f8dc8ff" rel="stylesheet" media="all">
     <link href="css/modal.css" rel="stylesheet" media="all">
     <link href="css/spinner.css" rel="stylesheet" media="all">
 
@@ -54,8 +54,8 @@
     <script src="js/jquery-1.12.4.min.js" integrity="sha256-ZosEbRLbNQzLpnKIkEdrPv7lOy9C27hHQ+Xp8a4MxAQ="></script>
     <!-- Include all compiled plugins (below), or include individual files as needed -->
     <script src="js/bootstrap.min.js"></script>
-    <script src="js/config.js?revision=f3a4716"></script>
-    <script src="js/ponymail.js?revision=f3a4716"></script>
-    <script src="js/oauth.js?revision=f3a4716"></script>
+    <script src="js/config.js?revision=f8dc8ff"></script>
+    <script src="js/ponymail.js?revision=f8dc8ff"></script>
+    <script src="js/oauth.js?revision=f8dc8ff"></script>
   </body>
 </html>
diff --git a/webui/thread.html b/webui/thread.html
index d229418..70577ae 100644
--- a/webui/thread.html
+++ b/webui/thread.html
@@ -25,7 +25,7 @@
     <!-- Bootstrap -->
     
     <link href="css/bootstrap.min.css" rel="stylesheet" media="all">
-    <link href="css/scaffolding.css?revision=f3a4716" rel="stylesheet" media="all">
+    <link href="css/scaffolding.css?revision=f8dc8ff" rel="stylesheet" media="all">
     <link href="css/modal.css" rel="stylesheet" media="all">
     <link href="css/spinner.css" rel="stylesheet" media="all">
     <link rel="alternate" href="/api/static.lua"/>
@@ -97,9 +97,9 @@
     <script src="js/jquery-1.12.4.min.js" integrity="sha256-ZosEbRLbNQzLpnKIkEdrPv7lOy9C27hHQ+Xp8a4MxAQ="></script>
     <!-- Include all compiled plugins (below), or include individual files as needed -->
     <script src="js/bootstrap.min.js"></script>
-    <script src="js/config.js?revision=f3a4716"></script>
-    <script src="js/wordcloud.js?revision=f3a4716"></script>
-    <script src="js/ponymail.js?revision=f3a4716"></script>
+    <script src="js/config.js?revision=f8dc8ff"></script>
+    <script src="js/wordcloud.js?revision=f8dc8ff"></script>
+    <script src="js/ponymail.js?revision=f8dc8ff"></script>
     <div id="splash" class="splash fade-in"> &nbsp; </div>
     <div style="clear: both;"></div>
   </body>