Initial fixes
This commit is contained in:
parent
146483b3c4
commit
694ddece28
|
@ -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
|
||||
}
|
||||
}
|
||||
```
|
|
@ -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")
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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")
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
});
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
Loading…
Reference in New Issue