Initial fixes

This commit is contained in:
Johannes Frohnmeyer 2022-10-22 23:29:07 +02:00
parent 146483b3c4
commit 694ddece28
Signed by: Johannes
GPG Key ID: E76429612C2929F4
15 changed files with 242 additions and 146 deletions

34
README.md Normal file
View File

@ -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
}
}
```

View File

@ -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")

View File

@ -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;
}
}
}

View File

@ -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 {

View File

@ -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 <TEx extends Throwable> void forEachSubscription(ThrowingConsumer<Subscription, TEx> 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<Subscription> 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")
);
}

View File

@ -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) {
}

View File

@ -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;
}
}

View File

@ -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<WrappedMessage> 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 {

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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<WrappedMessage>, 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<WrappedMessage> iterator() {
try {
return Arrays.stream(fld.getMessages())
.map(s -> new WrappedMessage((MimeMessage) s, address, mailInterface))
.iterator();
} catch (MessagingException e) {
throw new UncheckedMessagingException(e);
}
}
}

View File

@ -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<String, String> 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;
});

View File

@ -0,0 +1,2 @@
<h1>Could not close this issue!</h1>
<p>You must be the original author of an issue to close it</p>

View File

@ -1,4 +1,4 @@
<h1>Create issue</h1>
<h1>Creating an issue</h1>
<p>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.</p>
<p>You will receive a timely response containing a link to the created issue.</p>
<p>Additionally, you will be informed if any comments are posted on the issue and may add your own by replying to them</p>

View File

@ -4,8 +4,8 @@
<link rel="stylesheet" href="theme.css" type="text/css">
</head>
<body>
<div style="text-align: center;">
%s
<div style="text-align: center;margin: 0;position: absolute;top: 50%;left: 50%;transform: translate(-50%, -50%);">
<%template-content%>
</div>
</body>
</html>