package io.gitlab.jfronny.gitea.helpdesk; import io.gitlab.jfronny.commons.StringFormatter; 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.*; import io.gitlab.jfronny.gitea.helpdesk.web.WebInterface; import jakarta.mail.MessagingException; import org.commonmark.parser.Parser; import org.commonmark.renderer.html.HtmlRenderer; import java.io.FileNotFoundException; import java.io.IOException; import java.util.List; public record UpdateTask(DBInterface db, MailInterface mail, GiteaInterface gitea, WebInterface web, List ignored) implements Runnable { private static final Parser MARKDOWN_PARSER = Parser.builder().build(); private static final HtmlRenderer MARKDOWN_RENDER = HtmlRenderer.builder().build(); 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 template(String body) { return TEMPLATE.formatted(WebInterface.THEME, body); } @Override public void run() { try { updateSubscriptions(); processEmails(); } catch (Throwable e) { Main.LOG.error("Could not run update task", e); } } private void updateSubscriptions() throws Exception { String[] addressParts = mail.getAddress().split("@"); db.forEachSubscription(subscription -> { ThrowingBiConsumer reply = (content, subject) -> { String[] previousMessages = subscription.referenceChain().split(" "); String previousMessageId = previousMessages[previousMessages.length - 1]; mail.reply(addressParts[0] + "+reply+" + subscription.id(), subscription.email(), content, subject, previousMessageId, subscription.referenceChain(), null); }; GiteaIssue issue; try { issue = gitea.getIssue(subscription.repoOwner(), subscription.repo(), subscription.issue()); } catch (FileNotFoundException fe) { reply.accept(template(MAIL_ISSUE_DELETED), "Issue deleted"); db.removeSubscription(subscription.id()); return; } if (issue.state.equals("closed")) { reply.accept(template(MAIL_ISSUE_CLOSED), "Issue closed"); db.removeSubscription(subscription.id()); } for (GiteaIssueComment comment : gitea.getComments(subscription.repoOwner(), subscription.repo(), subscription.issue())) { if (comment.id > subscription.issueComment()) { reply.accept(MARKDOWN_RENDER.render(MARKDOWN_PARSER.parse(comment.body)), issue.title); db.updateSubscriptionIssueComment(subscription.id(), comment.id); } } }); } private void processEmails() throws MessagingException { String[] addressParts = mail.getAddress().split("@"); try (WrappedMessageSet wms = mail.getInbox()) { for (WrappedMessage message : wms) { try { if (ignored.contains(message.getSender())) { message.delete(); continue; } 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.getMessageID(), true); String unsubscribeUrl = web.getAddress() + "/unsubscribe?id=" + id; String closeUrl = web.getAddress() + "/close?id=" + id; message.reply(addressParts[0] + "+reply+" + id + '@' + addressParts[1], template(MAIL_CREATE.formatted(issue.url, unsubscribeUrl, closeUrl))); } 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.getMessageID()); } 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"); } long issueComment = gitea.addComment(owner, repo, issueId, formatBody(message)).id; if (issue.state.equals("closed")) { message.reply(mail.getAddress(), template(MAIL_COMMENT_CLOSED.formatted(issue.url))); } else { String id = db.addSubscription(message.getSender(), owner, repo, issue.id, issueComment, message.getMessageID(), false); String unsubscribeUrl = web.getAddress() + "/unsubscribe?id=" + id; message.reply(addressParts[0] + "+reply+" + id + '@' + addressParts[1], template(MAIL_COMMENT.formatted(issue.url, unsubscribeUrl))); } } 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(); } } } } private void checkArgs(String owner, String repo) throws UnexpectedMailException { 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 { return "Submitted by " + message.getSenderName() + ":\n\n" + message.getText(); } }