From 694ddece28726674815650d3c98ecf98651cad10 Mon Sep 17 00:00:00 2001 From: JFronny Date: Sat, 22 Oct 2022 23:29:07 +0200 Subject: [PATCH] Initial fixes --- README.md | 34 +++++ build.gradle.kts | 6 +- .../gitlab/jfronny/gitea/helpdesk/Main.java | 7 +- .../jfronny/gitea/helpdesk/UpdateTask.java | 131 +++++++++--------- .../gitea/helpdesk/db/DBInterface.java | 55 +++++--- .../gitea/helpdesk/db/Subscription.java | 2 +- .../helpdesk/db/UncheckedSQLException.java | 15 -- .../gitea/helpdesk/mail/MailInterface.java | 14 +- .../mail/UncheckedMessagingException.java | 15 ++ .../gitea/helpdesk/mail/WrappedMessage.java | 9 +- .../helpdesk/mail/WrappedMessageSet.java | 38 +++++ .../gitea/helpdesk/web/WebInterface.java | 54 ++++---- src/main/resources/web/close/not_creator.html | 2 + src/main/resources/web/issue/success.html | 2 +- src/main/resources/web/template.html | 4 +- 15 files changed, 242 insertions(+), 146 deletions(-) create mode 100644 README.md delete mode 100644 src/main/java/io/gitlab/jfronny/gitea/helpdesk/db/UncheckedSQLException.java create mode 100644 src/main/java/io/gitlab/jfronny/gitea/helpdesk/mail/UncheckedMessagingException.java create mode 100644 src/main/java/io/gitlab/jfronny/gitea/helpdesk/mail/WrappedMessageSet.java create mode 100644 src/main/resources/web/close/not_creator.html diff --git a/README.md b/README.md new file mode 100644 index 0000000..bb838ec --- /dev/null +++ b/README.md @@ -0,0 +1,34 @@ +# Gitea-Helpdesk +Gitea-Helpdesk is an extension to Gitea that allows users to create issues through emails, comment on existing ones and receiving notifications for new comments. +To do so, it must be connected to a postgresql database, a gitea instance and a mail server (pop3+smtp). +The mail server MUST support sub-addressing with `+` and the repository name and gitea username must match `[a-zA-Z0-9-_]+`. +To set up the service, build a jar and add a config file as follows to the current directory: +```json5 +{ + "gitea": { + "host": "https://gitea.example.com", // Your gitea instance + "token": "0123456789abcdef0123456789abcdef12345678" // An access token + }, + "email": { + "address": "helpdesk@example.com", // The e-mail address of the help desk + "waitTime": 10, // The amount of seconds to wait between each iteration + "smtp": { + "host": "smtp.example.com:465", // The address of the SMTP server + "password": "1234", // The password for login (the username is the address) + "ssl": true // Whether to enable ssl encryption + }, + "pop3": { + "host": "pop3.example.com:995", // The address of the POP3 server + "password": "1234", // The password for login (the username is the address) + "ssl": true // Whether to enable ssl encryption + } + }, + "database": { + "connectionString": "postgresql://localhost/helpdesk?user=helpdesk" // The postgresql connection string (see postgresql jdbc documentation, sockets are supported) + }, + "web": { + "port": 80, // The port to host the website at + "publicAddress": "http://127.0.0.1:80" // Where users will access the website + } +} +``` \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 3d197d6..d5eeb4b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -20,10 +20,12 @@ dependencies { implementation("com.sun.mail:jakarta.mail:2.0.1") implementation("org.postgresql:postgresql:42.5.0") implementation("org.apache.commons:commons-dbcp2:2.9.0") - implementation("io.gitlab.jfronny:commons:2022.10.22+18-33-39") - implementation("io.gitlab.jfronny:commons-gson:2022.10.22+18-33-39") + implementation("io.gitlab.jfronny:commons:2022.10.22+20-29-33") + implementation("io.gitlab.jfronny:commons-gson:2022.10.22+20-29-33") implementation("com.kohlschutter.junixsocket:junixsocket-core:2.6.0") implementation("org.jsoup:jsoup:1.15.3") implementation("net.freeutils:jlhttp:2.6") compileOnly("org.jetbrains:annotations:23.0.0") } + +tasks.run.get().jvmArgs("-Djava.util.logging.config.file=logging.properties") \ No newline at end of file diff --git a/src/main/java/io/gitlab/jfronny/gitea/helpdesk/Main.java b/src/main/java/io/gitlab/jfronny/gitea/helpdesk/Main.java index c0f28ce..e753a9c 100644 --- a/src/main/java/io/gitlab/jfronny/gitea/helpdesk/Main.java +++ b/src/main/java/io/gitlab/jfronny/gitea/helpdesk/Main.java @@ -22,9 +22,9 @@ import java.util.stream.Collectors; // https://www.javatpoint.com/example-of-receiving-attachment-with-email-using-java-mail-api public class Main { public static final Logger LOG = Logger.forName("Gitea-Helpdesk"); - public static final Pattern MAIL_PATTERN = Pattern.compile("(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|\"(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21\\x23-\\x5b\\x5d-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])*\")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\\[(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])\\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21-\\x5a\\x53-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])+)])\n"); + public static final Pattern MAIL_PATTERN = Pattern.compile("(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|\"(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21\\x23-\\x5b\\x5d-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])*\")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\\[(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])\\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21-\\x5a\\x53-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])+)])"); public static final Pattern HOST_PATTERN = Pattern.compile("(?:[0-9a-zA-Z-._~]+|[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3})(?::[0-9]{1,5})?"); - public static final Pattern ALPHANUMERIC_PATTERN = Pattern.compile("[a-zA-Z0-9]+"); + public static final Pattern PATH_SEGMENT_PATTERN = Pattern.compile("[a-zA-Z0-9-_]+"); public static void main(String[] args) throws IOException, SQLException, MessagingException, Config.IllegalConfigException, InterruptedException { GsonHolders.registerSerializer(); @@ -54,6 +54,9 @@ public class Main { return br.lines().collect(Collectors.joining("\n")); } catch (IOException e) { throw new RuntimeException(e); + } catch (NullPointerException e) { + LOG.error("Could not get resource: " + path); + throw e; } } } diff --git a/src/main/java/io/gitlab/jfronny/gitea/helpdesk/UpdateTask.java b/src/main/java/io/gitlab/jfronny/gitea/helpdesk/UpdateTask.java index 5ddbb78..becd4e5 100644 --- a/src/main/java/io/gitlab/jfronny/gitea/helpdesk/UpdateTask.java +++ b/src/main/java/io/gitlab/jfronny/gitea/helpdesk/UpdateTask.java @@ -5,27 +5,25 @@ import io.gitlab.jfronny.commons.throwable.ThrowingBiConsumer; import io.gitlab.jfronny.gitea.helpdesk.db.DBInterface; import io.gitlab.jfronny.gitea.helpdesk.db.Subscription; import io.gitlab.jfronny.gitea.helpdesk.gitea.*; -import io.gitlab.jfronny.gitea.helpdesk.mail.MailInterface; -import io.gitlab.jfronny.gitea.helpdesk.mail.WrappedMessage; +import io.gitlab.jfronny.gitea.helpdesk.mail.*; import io.gitlab.jfronny.gitea.helpdesk.web.WebInterface; import jakarta.mail.MessagingException; import java.io.FileNotFoundException; import java.io.IOException; -import java.sql.SQLException; public record UpdateTask(DBInterface db, MailInterface mail, GiteaInterface gitea, WebInterface web) implements Runnable { - private static final String TEMPLATE = Main.getResource("mail/template.html").formatted(WebInterface.THEME, "%s"); - private static final String MAIL_ERROR = mail("error.html"); - private static final String MAIL_UNEXPECTED = mail("unexpected.html"); - private static final String MAIL_CREATE = mail("create.html"); - private static final String MAIL_COMMENT = mail("comment.html"); - private static final String MAIL_COMMENT_CLOSED = mail("comment_closed.html"); - private static final String MAIL_ISSUE_DELETED = mail("issue_deleted.html"); - private static final String MAIL_ISSUE_CLOSED = mail("issue_closed.html"); + private static final String TEMPLATE = Main.getResource("/mail/template.html"); + private static final String MAIL_ERROR = Main.getResource("/mail/error.html"); + private static final String MAIL_UNEXPECTED = Main.getResource("/mail/unexpected.html"); + private static final String MAIL_CREATE = Main.getResource("/mail/create.html"); + private static final String MAIL_COMMENT = Main.getResource("/mail/comment.html"); + private static final String MAIL_COMMENT_CLOSED = Main.getResource("/mail/comment_closed.html"); + private static final String MAIL_ISSUE_DELETED = Main.getResource("/mail/issue_deleted.html"); + private static final String MAIL_ISSUE_CLOSED = Main.getResource("/mail/issue_closed.html"); - private static String mail(String path) { - return TEMPLATE.formatted(Main.getResource("mail/" + path)); + private static String template(String body) { + return TEMPLATE.formatted(WebInterface.THEME, body); } @Override @@ -33,7 +31,7 @@ public record UpdateTask(DBInterface db, MailInterface mail, GiteaInterface gite try { updateSubscriptions(); processEmails(); - } catch (Exception e) { + } catch (Throwable e) { Main.LOG.error("Could not run update task", e); } } @@ -51,12 +49,12 @@ public record UpdateTask(DBInterface db, MailInterface mail, GiteaInterface gite try { issue = gitea.getIssue(subscription.repoOwner(), subscription.repo(), subscription.issue()); } catch (FileNotFoundException fe) { - reply.accept(MAIL_ISSUE_DELETED, "Issue deleted"); + reply.accept(template(MAIL_ISSUE_DELETED), "Issue deleted"); db.removeSubscription(subscription.id()); return; } if (issue.state.equals("closed")) { //TODO test - reply.accept(MAIL_ISSUE_CLOSED, "Issue closed"); + reply.accept(template(MAIL_ISSUE_CLOSED), "Issue closed"); db.removeSubscription(subscription.id()); } for (GiteaIssueComment comment : gitea.getComments(subscription.repoOwner(), subscription.repo(), subscription.issue())) { @@ -68,65 +66,68 @@ public record UpdateTask(DBInterface db, MailInterface mail, GiteaInterface gite }); } - private void processEmails() throws SQLException, MessagingException, IOException { + private void processEmails() throws MessagingException { String[] addressParts = mail.getAddress().split("@"); - for (WrappedMessage message : mail.getInbox()) { - try { - String[] args = message.getRecipientSubAddress().split("\\+"); - switch (args[0]) { - case "create" -> { - if (args.length != 3) throw new UnexpectedMailException("Create classifier only allows two parameters"); - String owner = args[1]; - String repo = args[2]; - checkArgs(owner, repo); - GiteaIssue issue = gitea.createIssue(owner, repo, message.getSubject(), formatBody(message)); - String id = db.addSubscription(message.getSender(), owner, repo, issue.id, 0, message.getSender()); - String unsubscribeUrl = web.getAddress() + "/unsubscribe?id=" + id; //TODO test - message.reply(addressParts[0] + "+reply+" + owner + "+" + repo + "+" + issue.id, MAIL_CREATE.formatted(issue.url, unsubscribeUrl)); - } - case "reply" -> { - if (args.length != 2) throw new UnexpectedMailException("Reply classifier only allows one parameter"); - Subscription sub = db.getSubscription(args[1]).orElseThrow(() -> new UnexpectedMailException("Reply classifier does not represent an active issue")); - GiteaIssueComment commentId = gitea.addComment(sub.repoOwner(), sub.repo(), sub.issue(), formatBody(message)); - db.updateSubscriptionIssueComment(sub.id(), commentId.id); - db.updateSubscriptionReferenceChain(sub.id(), sub.referenceChain() + " " + message.getSender()); - } - case "comment" -> { - if (args.length == 4) throw new UnexpectedMailException("Comment classifier requires four parameters"); - String owner = args[1]; - String repo = args[2]; - long issueId = Long.parseLong(args[3]); - checkArgs(owner, repo); - GiteaIssue issue; - try { - issue = gitea.getIssue(owner, repo, issueId); - } catch (FileNotFoundException fe) { - throw new UnexpectedMailException("This issue does not exist"); - } - gitea.addComment(owner, repo, issueId, formatBody(message)); - if (issue.state.equals("closed")) { - message.reply(addressParts[0] + "+reply+" + owner + "+" + repo + "+" + issue.id, MAIL_COMMENT_CLOSED.formatted(issue.url)); - } else { - String id = db.addSubscription(message.getSender(), owner, repo, issue.id, 0, message.getSender()); + try (WrappedMessageSet wms = mail.getInbox()) { + for (WrappedMessage message : wms) { + try { + String[] args = message.getRecipientSubAddress().split("\\+"); + switch (args[0]) { + case "create" -> { + if (args.length != 3) throw new UnexpectedMailException("Create classifier only allows two parameters"); + String owner = args[1]; + String repo = args[2]; + checkArgs(owner, repo); + GiteaIssue issue = gitea.createIssue(owner, repo, message.getSubject(), formatBody(message)); + String id = db.addSubscription(message.getSender(), owner, repo, issue.id, 0, message.getSender(), true); String unsubscribeUrl = web.getAddress() + "/unsubscribe?id=" + id; //TODO test - message.reply(addressParts[0] + "+reply+" + owner + "+" + repo + "+" + issue.id, MAIL_COMMENT.formatted(issue.url, unsubscribeUrl)); + message.reply(addressParts[0] + "+reply+" + owner + "+" + repo + "+" + issue.id + '@' + addressParts[1], template(MAIL_CREATE.formatted(issue.url, unsubscribeUrl))); } + case "reply" -> { + if (args.length != 2) throw new UnexpectedMailException("Reply classifier only allows one parameter"); + Subscription sub = db.getSubscription(args[1]).orElseThrow(() -> new UnexpectedMailException("Reply classifier does not represent an active issue")); + GiteaIssueComment commentId = gitea.addComment(sub.repoOwner(), sub.repo(), sub.issue(), formatBody(message)); + db.updateSubscriptionIssueComment(sub.id(), commentId.id); + db.updateSubscriptionReferenceChain(sub.id(), sub.referenceChain() + " " + message.getSender()); + } + case "comment" -> { + if (args.length == 4) throw new UnexpectedMailException("Comment classifier requires four parameters"); + String owner = args[1]; + String repo = args[2]; + long issueId = Long.parseLong(args[3]); + checkArgs(owner, repo); + GiteaIssue issue; + try { + issue = gitea.getIssue(owner, repo, issueId); + } catch (FileNotFoundException fe) { + throw new UnexpectedMailException("This issue does not exist"); + } + gitea.addComment(owner, repo, issueId, formatBody(message)); + if (issue.state.equals("closed")) { + message.reply(addressParts[0] + "+reply+" + owner + "+" + repo + "+" + issue.id + '@' + addressParts[1], template(MAIL_COMMENT_CLOSED.formatted(issue.url))); + } else { + String id = db.addSubscription(message.getSender(), owner, repo, issue.id, 0, message.getSender(), false); + String unsubscribeUrl = web.getAddress() + "/unsubscribe?id=" + id; //TODO test + message.reply(addressParts[0] + "+reply+" + owner + "+" + repo + "+" + issue.id + '@' + addressParts[1], template(MAIL_COMMENT.formatted(issue.url, unsubscribeUrl))); + } + } + default -> throw new UnexpectedMailException("Did not expect classifier " + args[0]); } - default -> throw new UnexpectedMailException("Did not expect classifier " + args[0]); + } catch (UnexpectedMailException | NumberFormatException t) { + message.reply(mail.getAddress(), template(MAIL_UNEXPECTED.formatted(WebInterface.escapeHTML(StringFormatter.toString(t))))); + } catch (Throwable t) { + Main.LOG.error("Could not parse mail", t); + message.reply(mail.getAddress(), template(MAIL_ERROR.formatted(WebInterface.escapeHTML(StringFormatter.toString(t))))); + } finally { + message.delete(); } - } catch (UnexpectedMailException | NumberFormatException t) { - message.reply(mail.getAddress(), MAIL_UNEXPECTED.formatted(WebInterface.escapeHTML(StringFormatter.toString(t)))); - } catch (Throwable t) { - Main.LOG.error("Could not parse mail", t); - message.reply(mail.getAddress(), MAIL_ERROR.formatted(WebInterface.escapeHTML(StringFormatter.toString(t)))); } } } private void checkArgs(String owner, String repo) throws UnexpectedMailException { - //TODO document this limitation - if (!Main.ALPHANUMERIC_PATTERN.matcher(owner).matches()) throw new UnexpectedMailException("Unsupported owner string"); - if (!Main.ALPHANUMERIC_PATTERN.matcher(repo).matches()) throw new UnexpectedMailException("Unsupported repo string"); + if (!Main.PATH_SEGMENT_PATTERN.matcher(owner).matches()) throw new UnexpectedMailException("Unsupported owner string"); + if (!Main.PATH_SEGMENT_PATTERN.matcher(repo).matches()) throw new UnexpectedMailException("Unsupported repo string"); } private String formatBody(WrappedMessage message) throws UnexpectedMailException, MessagingException, IOException { diff --git a/src/main/java/io/gitlab/jfronny/gitea/helpdesk/db/DBInterface.java b/src/main/java/io/gitlab/jfronny/gitea/helpdesk/db/DBInterface.java index 9457fda..605b038 100644 --- a/src/main/java/io/gitlab/jfronny/gitea/helpdesk/db/DBInterface.java +++ b/src/main/java/io/gitlab/jfronny/gitea/helpdesk/db/DBInterface.java @@ -15,9 +15,14 @@ public class DBInterface implements AutoCloseable { ds.setMinIdle(1); ds.setMaxIdle(10); ds.setMaxOpenPreparedStatements(100); - try (Statement st = ds.getConnection().createStatement()) { - st.execute(""" - CREATE OR REPLACE FUNCTION generate_uid(size INT) RETURNS TEXT AS $$ + ds.setAutoCommitOnReturn(true); + ds.setDefaultAutoCommit(true); + ds.start(); + try (Connection cx = ds.getConnection()) { + try (Statement st = cx.createStatement()) { + st.executeUpdate(""" + CREATE EXTENSION pgcrypto; + CREATE OR REPLACE FUNCTION helpdesk_generate_uid(size INT) RETURNS TEXT AS $$ DECLARE characters TEXT := 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; bytes BYTEA := gen_random_bytes(size); @@ -31,33 +36,36 @@ public class DBInterface implements AutoCloseable { END LOOP; RETURN output; END; - $$ LANGUAGE plpgsql VOLATILE;"""); - st.execute(""" - create table if not exists subscriptions( - id text primary key default generate_uid(20), + $$ LANGUAGE plpgsql VOLATILE; + create table if not exists helpdesk_subscriptions( + id text primary key default helpdesk_generate_uid(20), email text, repo_owner text, repo text, issue bigint, issue_comment bigint, - reference_chain text - )"""); + reference_chain text, + creator bool + );"""); + } } } public void forEachSubscription(ThrowingConsumer action) throws SQLException, TEx { - try (Statement st = ds.getConnection().createStatement(); - ResultSet rs = st.executeQuery("select * from subscriptions")) { + try (Connection cx = ds.getConnection(); + Statement st = cx.createStatement(); + ResultSet rs = st.executeQuery("select * from helpdesk_subscriptions")) { while (rs.next()) { action.accept(get(rs)); } } } - public String addSubscription(String email, String repoOwner, String repo, long issue, long issueComment, String referenceChain) throws SQLException { - try (PreparedStatement st = ds.getConnection().prepareStatement(""" - insert into subscriptions (email, repo_owner, repo, issue, issue_comment, reference_chain) - values (?, ?, ?, ?, ?, ?) + public String addSubscription(String email, String repoOwner, String repo, long issue, long issueComment, String referenceChain, boolean creator) throws SQLException { + try (Connection cx = ds.getConnection(); + PreparedStatement st = cx.prepareStatement(""" + insert into helpdesk_subscriptions (email, repo_owner, repo, issue, issue_comment, reference_chain, creator) + values (?, ?, ?, ?, ?, ?, ?) returning id""")) { st.setString(1, email); st.setString(2, repoOwner); @@ -65,6 +73,7 @@ public class DBInterface implements AutoCloseable { st.setLong(4, issue); st.setLong(5, issueComment); st.setString(6, referenceChain); + st.setBoolean(7, creator); try (ResultSet rs = st.executeQuery()) { rs.next(); return rs.getString("id"); @@ -73,8 +82,9 @@ public class DBInterface implements AutoCloseable { } public Optional getSubscription(String id) throws SQLException { - try (PreparedStatement st = ds.getConnection() - .prepareStatement("select id, email, repo_owner, repo, issue, issue_comment, reference_chain from subscriptions where id = ?")) { + try (Connection cx = ds.getConnection(); + PreparedStatement st = cx + .prepareStatement("select id, email, repo_owner, repo, issue, issue_comment, reference_chain, creator from helpdesk_subscriptions where id = ?")) { st.setString(1, id); try (ResultSet rs = st.executeQuery()) { if (rs.next()) { @@ -85,14 +95,16 @@ public class DBInterface implements AutoCloseable { } public void removeSubscription(String id) throws SQLException { - try (PreparedStatement st = ds.getConnection().prepareStatement("delete from subscriptions where id = ?")) { + try (Connection cx = ds.getConnection(); + PreparedStatement st = cx.prepareStatement("delete from helpdesk_subscriptions where id = ?")) { st.setString(1, id); st.executeUpdate(); } } public void updateSubscriptionIssueComment(String id, long issueComment) throws SQLException { - try (PreparedStatement st = ds.getConnection().prepareStatement("update subscriptions set issue_comment = ? where id = ?")) { + try (Connection cx = ds.getConnection(); + PreparedStatement st = cx.prepareStatement("update helpdesk_subscriptions set issue_comment = ? where id = ?")) { st.setLong(1, issueComment); st.setString(2, id); st.executeUpdate(); @@ -100,7 +112,7 @@ public class DBInterface implements AutoCloseable { } public void updateSubscriptionReferenceChain(String id, String referenceChain) throws SQLException { - try (PreparedStatement st = ds.getConnection().prepareStatement("update subscriptions set reference_chain = ? where id = ?")) { + try (Connection cx = ds.getConnection(); PreparedStatement st = cx.prepareStatement("update helpdesk_subscriptions set reference_chain = ? where id = ?")) { st.setString(1, referenceChain); st.setString(2, id); st.executeUpdate(); @@ -115,7 +127,8 @@ public class DBInterface implements AutoCloseable { rs.getString("repo"), rs.getLong("issue"), rs.getLong("issue_comment"), - rs.getString("reference_chain") + rs.getString("reference_chain"), + rs.getBoolean("creator") ); } diff --git a/src/main/java/io/gitlab/jfronny/gitea/helpdesk/db/Subscription.java b/src/main/java/io/gitlab/jfronny/gitea/helpdesk/db/Subscription.java index 45e94c4..0f40152 100644 --- a/src/main/java/io/gitlab/jfronny/gitea/helpdesk/db/Subscription.java +++ b/src/main/java/io/gitlab/jfronny/gitea/helpdesk/db/Subscription.java @@ -1,4 +1,4 @@ package io.gitlab.jfronny.gitea.helpdesk.db; -public record Subscription(String id, String email, String repoOwner, String repo, long issue, long issueComment, String referenceChain) { +public record Subscription(String id, String email, String repoOwner, String repo, long issue, long issueComment, String referenceChain, boolean creator) { } diff --git a/src/main/java/io/gitlab/jfronny/gitea/helpdesk/db/UncheckedSQLException.java b/src/main/java/io/gitlab/jfronny/gitea/helpdesk/db/UncheckedSQLException.java deleted file mode 100644 index 32bcfba..0000000 --- a/src/main/java/io/gitlab/jfronny/gitea/helpdesk/db/UncheckedSQLException.java +++ /dev/null @@ -1,15 +0,0 @@ -package io.gitlab.jfronny.gitea.helpdesk.db; - -import java.sql.SQLException; - -public class UncheckedSQLException extends RuntimeException { - private final SQLException exception; - - public UncheckedSQLException(SQLException exception) { - this.exception = exception; - } - - public SQLException getException() { - return exception; - } -} diff --git a/src/main/java/io/gitlab/jfronny/gitea/helpdesk/mail/MailInterface.java b/src/main/java/io/gitlab/jfronny/gitea/helpdesk/mail/MailInterface.java index 151ff92..4eb6bdf 100644 --- a/src/main/java/io/gitlab/jfronny/gitea/helpdesk/mail/MailInterface.java +++ b/src/main/java/io/gitlab/jfronny/gitea/helpdesk/mail/MailInterface.java @@ -6,8 +6,7 @@ import jakarta.mail.*; import jakarta.mail.internet.*; import org.jsoup.Jsoup; -import java.util.*; -import java.util.stream.Collectors; +import java.util.Properties; public class MailInterface implements AutoCloseable { private final Config.EMail config; @@ -57,13 +56,10 @@ public class MailInterface implements AutoCloseable { return lastPop3; } - public Set getInbox() throws MessagingException { - try (Folder inbox = getStore().getFolder("INBOX")) { - inbox.open(Folder.READ_ONLY); - return Arrays.stream(inbox.getMessages()) - .map(s -> new WrappedMessage((MimeMessage) s, config.address, this)) - .collect(Collectors.toSet()); - } + public WrappedMessageSet getInbox() throws MessagingException { + Folder inbox = getStore().getFolder("INBOX"); + inbox.open(Folder.READ_WRITE); + return new WrappedMessageSet(inbox, config.address, this); } public void reply(String from, String to, String content, String subject, String previousMessageId, String previousReferences, String previousInReplyTo) throws MessagingException { diff --git a/src/main/java/io/gitlab/jfronny/gitea/helpdesk/mail/UncheckedMessagingException.java b/src/main/java/io/gitlab/jfronny/gitea/helpdesk/mail/UncheckedMessagingException.java new file mode 100644 index 0000000..1ee2a12 --- /dev/null +++ b/src/main/java/io/gitlab/jfronny/gitea/helpdesk/mail/UncheckedMessagingException.java @@ -0,0 +1,15 @@ +package io.gitlab.jfronny.gitea.helpdesk.mail; + +import jakarta.mail.MessagingException; + +public class UncheckedMessagingException extends RuntimeException { + private final MessagingException exception; + + public UncheckedMessagingException(MessagingException exception) { + this.exception = exception; + } + + public MessagingException getException() { + return exception; + } +} diff --git a/src/main/java/io/gitlab/jfronny/gitea/helpdesk/mail/WrappedMessage.java b/src/main/java/io/gitlab/jfronny/gitea/helpdesk/mail/WrappedMessage.java index 64517d6..765b320 100644 --- a/src/main/java/io/gitlab/jfronny/gitea/helpdesk/mail/WrappedMessage.java +++ b/src/main/java/io/gitlab/jfronny/gitea/helpdesk/mail/WrappedMessage.java @@ -36,10 +36,7 @@ public class WrappedMessage { static String getText(Part p) throws MessagingException, IOException { if (p.isMimeType("text/*")) { - String s = (String)p.getContent(); - // p.isMimeType("text/html") ? Jsoup.parse(s).text() : s - //TODO ensure this works - return s; + return (String)p.getContent(); } if (p.isMimeType("multipart/alternative")) { // prefer html text over plain text @@ -105,4 +102,8 @@ public class WrappedMessage { message.getHeader("In-Reply-To", "") ); } + + public void delete() throws MessagingException { + message.setFlag(Flags.Flag.DELETED, true); + } } diff --git a/src/main/java/io/gitlab/jfronny/gitea/helpdesk/mail/WrappedMessageSet.java b/src/main/java/io/gitlab/jfronny/gitea/helpdesk/mail/WrappedMessageSet.java new file mode 100644 index 0000000..f85584c --- /dev/null +++ b/src/main/java/io/gitlab/jfronny/gitea/helpdesk/mail/WrappedMessageSet.java @@ -0,0 +1,38 @@ +package io.gitlab.jfronny.gitea.helpdesk.mail; + +import jakarta.mail.Folder; +import jakarta.mail.MessagingException; +import jakarta.mail.internet.MimeMessage; +import org.jetbrains.annotations.NotNull; + +import java.util.Arrays; +import java.util.Iterator; + +public class WrappedMessageSet implements Iterable, AutoCloseable { + private final Folder fld; + private final String address; + private final MailInterface mailInterface; + + public WrappedMessageSet(Folder fld, String address, MailInterface mailInterface) { + this.fld = fld; + this.address = address; + this.mailInterface = mailInterface; + } + + @Override + public void close() throws MessagingException { + fld.close(); + } + + @NotNull + @Override + public Iterator iterator() { + try { + return Arrays.stream(fld.getMessages()) + .map(s -> new WrappedMessage((MimeMessage) s, address, mailInterface)) + .iterator(); + } catch (MessagingException e) { + throw new UncheckedMessagingException(e); + } + } +} diff --git a/src/main/java/io/gitlab/jfronny/gitea/helpdesk/web/WebInterface.java b/src/main/java/io/gitlab/jfronny/gitea/helpdesk/web/WebInterface.java index bcff9c1..cdd868e 100644 --- a/src/main/java/io/gitlab/jfronny/gitea/helpdesk/web/WebInterface.java +++ b/src/main/java/io/gitlab/jfronny/gitea/helpdesk/web/WebInterface.java @@ -14,18 +14,20 @@ import java.nio.charset.StandardCharsets; import java.util.Map; public class WebInterface implements AutoCloseable { - public static final String THEME = Main.getResource("web/theme.css"); - private static final String TEMPLATE = Main.getResource("web/template.html"); - private static final String ISSUE_MISSING_REPO = page("issue/missing_repo.html"); - private static final String ISSUE_SUCCESS = page("issue/success.html"); - private static final String UNSUBSCRIBE_MISSING_ID = page("unsubscribe/missing_id.html"); - private static final String UNSUBSCRIBE_FAILURE = page("unsubscribe/failure.html"); - private static final String UNSUBSCRIBE_SUCCESS = page("unsubscribe/success.html"); - private static final String CLOSE_MISSING_ID = page("close/missing_id.html"); - private static final String CLOSE_FAILURE = page("close/failure.html"); - private static final String CLOSE_SUCCESS = page("close/success.html"); - private static String page(String path) { - return TEMPLATE.formatted(Main.getResource("web/" + path)); + public static final String THEME = Main.getResource("/web/theme.css"); + private static final String TEMPLATE = Main.getResource("/web/template.html"); + private static final String ISSUE_MISSING_REPO = Main.getResource("/web/issue/missing_repo.html"); + private static final String ISSUE_SUCCESS = Main.getResource("/web/issue/success.html"); + private static final String UNSUBSCRIBE_MISSING_ID = Main.getResource("/web/unsubscribe/missing_id.html"); + private static final String UNSUBSCRIBE_FAILURE = Main.getResource("/web/unsubscribe/failure.html"); + private static final String UNSUBSCRIBE_SUCCESS = Main.getResource("/web/unsubscribe/success.html"); + private static final String CLOSE_MISSING_ID = Main.getResource("/web/close/missing_id.html"); + private static final String CLOSE_FAILURE = Main.getResource("/web/close/failure.html"); + private static final String CLOSE_NOT_CREATOR = Main.getResource("/web/close/not_creator.html"); + private static final String CLOSE_SUCCESS = Main.getResource("/web/close/success.html"); + + private String template(String body) { + return TEMPLATE.replace("<%template-content%>", body); } private final HTTPServer server; @@ -38,13 +40,13 @@ public class WebInterface implements AutoCloseable { host.addContext("/unsubscribe", (req, resp) -> { String id = req.getParams().get("id"); if (id == null) { - resp.send(404, UNSUBSCRIBE_MISSING_ID); + resp.send(404, template(UNSUBSCRIBE_MISSING_ID)); } else { try { db.removeSubscription(id); - resp.send(202, UNSUBSCRIBE_SUCCESS); + resp.send(202, template(UNSUBSCRIBE_SUCCESS)); } catch (Throwable e) { - resp.send(500, UNSUBSCRIBE_FAILURE.formatted(escapeHTML(StringFormatter.toString(e)))); + resp.send(500, template(UNSUBSCRIBE_FAILURE.formatted(escapeHTML(StringFormatter.toString(e))))); } } return 0; @@ -52,13 +54,17 @@ public class WebInterface implements AutoCloseable { host.addContext("/close", ((req, resp) -> { String id = req.getParams().get("id"); if (id == null) { - resp.send(404, CLOSE_MISSING_ID); + resp.send(404, template(CLOSE_MISSING_ID)); } else { try { db.getSubscription(id).ifPresentOrElse(sub -> { try { - gitea.closeIssue(sub.repoOwner(), sub.repo(), sub.issue()); - resp.send(202, CLOSE_SUCCESS); + if (sub.creator()) { + gitea.closeIssue(sub.repoOwner(), sub.repo(), sub.issue()); + resp.send(202, template(CLOSE_SUCCESS)); + } else { + resp.send(403, template(CLOSE_NOT_CREATOR)); + } } catch (IOException e) { throw new UncheckedIOException(e); } catch (URISyntaxException e) { @@ -66,13 +72,13 @@ public class WebInterface implements AutoCloseable { } }, () -> { try { - resp.send(404, CLOSE_MISSING_ID); + resp.send(404, template(CLOSE_MISSING_ID)); } catch (IOException e) { throw new UncheckedIOException(e); } }); } catch (Throwable e) { - resp.send(500, CLOSE_FAILURE.formatted(escapeHTML(StringFormatter.toString(e)))); + resp.send(500, template(CLOSE_FAILURE.formatted(escapeHTML(StringFormatter.toString(e))))); } } return 0; @@ -82,11 +88,11 @@ public class WebInterface implements AutoCloseable { Map params = req.getParams(); String owner = params.get("owner"); String repo = params.get("repo"); - if (owner == null || repo == null || owner.contains("+") || repo.contains("+")) { - resp.send(404, ISSUE_MISSING_REPO); + if (owner == null || repo == null || !Main.PATH_SEGMENT_PATTERN.matcher(owner).matches() || !Main.PATH_SEGMENT_PATTERN.matcher(repo).matches()) { + resp.send(404, template(ISSUE_MISSING_REPO)); } else { - String mail = "mailto:create+" + addr[0] + '+' + owner + '+' + repo; - resp.send(200, ISSUE_SUCCESS.formatted(escapeHTML(mail))); + String mail = "mailto:" + addr[0] + "+create+" + owner + '+' + repo + '@' + addr[1]; + resp.send(200, template(ISSUE_SUCCESS.formatted(escapeHTML(mail)))); } return 0; }); diff --git a/src/main/resources/web/close/not_creator.html b/src/main/resources/web/close/not_creator.html new file mode 100644 index 0000000..e1941cd --- /dev/null +++ b/src/main/resources/web/close/not_creator.html @@ -0,0 +1,2 @@ +

Could not close this issue!

+

You must be the original author of an issue to close it

\ No newline at end of file diff --git a/src/main/resources/web/issue/success.html b/src/main/resources/web/issue/success.html index 2f4cfe7..b2413fa 100644 --- a/src/main/resources/web/issue/success.html +++ b/src/main/resources/web/issue/success.html @@ -1,4 +1,4 @@ -

Create issue

+

Creating an issue

By clicking on the following link, you agree that the name you use to send your E-Mail and its content will be displayed in the issue created by mailing.

You will receive a timely response containing a link to the created issue.

Additionally, you will be informed if any comments are posted on the issue and may add your own by replying to them

diff --git a/src/main/resources/web/template.html b/src/main/resources/web/template.html index 2cfcf6c..424486b 100644 --- a/src/main/resources/web/template.html +++ b/src/main/resources/web/template.html @@ -4,8 +4,8 @@ -
-%s +
+<%template-content%>
\ No newline at end of file