Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue # 78 | Feature: Add Email Contact Form Function for Java #235

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions java/email-contact-form/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Compiled class file
*.class

# Log file
*.log

# BlueJ files
*.ctxt

# Mobile Tools for Java (J2ME)
.mtj.tmp/

# Package Files #
*.jar
*.war
*.nar
*.ear
*.zip
*.tar.gz
*.rar

# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
replay_pid*
# Ignore Gradle project-specific cache directory
.gradle

# Ignore Gradle build output directory
build
100 changes: 100 additions & 0 deletions java/email-contact-form/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# 📬 Java Email Contact Form Function

Sends an email with the contents of a HTML form.

## 🧰 Usage

### GET /

HTML forms for interacting with the function.

### POST /

Submit form data to send an email

**Parameters**

| Name | Description | Location | Type | Sample Value |
| ------ | --------------------------------- | ---------- | ------ | -------------------------------- |
| \_next | URL for redirect after submission | Form Param | String | `https://mywebapp.org/success` |
| \* | Any form values to send in email | Form Param | String | `Hey, I'd like to get in touch!` |

**Response**

Sample `200` Response:

```text
Location: https://mywebapp.org/success
```

Sample `400` Response:

```text
Location: https://mywebapp.org/referer?error=Error+Description
```

## ⚙️ Configuration

| Setting | Value |
| ----------------- | --------------- |
| Runtime | Java (17) |
| Entrypoint | `src/Main.java` |
| Permissions | `any` |
| Timeout (Seconds) | 15 |

## 🔒 Environment Variables

### SMTP_HOST

The address of your SMTP server. Many STMP providers will provide this information in their documentation. Some popular providers include: Mailgun, SendGrid, and Gmail.

| Question | Answer |
| ------------ | ------------------ |
| Required | Yes |
| Sample Value | `smtp.mailgun.org` |

### SMTP_PORT

The port of your STMP server. Commnly used ports include `25`, `465`, and `587`.

| Question | Answer |
| ------------ | ------ |
| Required | Yes |
| Sample Value | `25` |

### SMTP_USERNAME

The username for your SMTP server. This is commonly your email address.

| Question | Answer |
| ------------ | ----------------------- |
| Required | Yes |
| Sample Value | `[email protected]` |

### SMTP_PASSWORD

The password for your SMTP server.

| Question | Answer |
| ------------ | --------------------- |
| Required | Yes |
| Sample Value | `5up3r5tr0ngP4ssw0rd` |

### SUBMIT_EMAIL

The email address to send form submissions to.

| Question | Answer |
| ------------ | ----------------- |
| Required | Yes |
| Sample Value | `[email protected]` |

### ALLOWED_ORIGINS

An optional comma-separated list of allowed origins for CORS (defaults to `*`). This is an important security measure to prevent malicious users from abusing your function.

| Question | Answer |
| ------------- | ------------------------------------------------------------------- |
| Required | No |
| Sample Value | `https://mywebapp.org,https://mywebapp.com` |
| Documentation | [MDN: CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) |
4 changes: 4 additions & 0 deletions java/email-contact-form/deps.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
dependencies {
implementation 'io.appwrite:sdk-for-kotlin:4.0.0'
implementation 'com.sun.mail:jakarta.mail:2.0.0'
}
54 changes: 54 additions & 0 deletions java/email-contact-form/src/Cors.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package io.openruntimes.java.src;

import io.openruntimes.java.RuntimeContext;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.HashMap;


public class Cors {

/**
* Returns true if the origin is allowed to make requests to this endpoint
*
* Parameters:
* context: Context object
*
* Returns:
* (boolean): True if the origin is allowed, False otherwise
*/
public static boolean isOriginPermitted(RuntimeContext context) {
String allowedOrigins = System.getenv("ALLOWED_ORIGINS");
if (allowedOrigins == null || allowedOrigins.equals("*")) {
return true;
}

List<String> allowedOriginsList = Arrays.asList(allowedOrigins.split(","));
String originHeader = context.getReq().getHeaders().get("origin");
return originHeader != null && allowedOriginsList.contains(originHeader);
}

/**
* Returns the CORS headers for the request
*
* Parameters:
* context: Context object
*
* Returns:
* (Map<String, String>): CORS headers
*/
public static Map<String, String> getCorsHeaders(RuntimeContext context) {
if (!context.getReq().getHeaders().containsKey("origin")) {
return new HashMap<>();
}

String allowedOrigins = System.getenv("ALLOWED_ORIGINS");
if (allowedOrigins == null || allowedOrigins.equals("*")) {
return new HashMap<>(Map.of("Access-Control-Allow-Origin", "*"));
}

String originHeader = context.getReq().getHeaders().get("origin");
return new HashMap<>(Map.of("Access-Control-Allow-Origin", originHeader));
}
}
7 changes: 7 additions & 0 deletions java/email-contact-form/src/ErrorCode.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package io.openruntimes.java.src;

public class ErrorCode {
public static final String INVALID_REQUEST = "invalid-request";
public static final String MISSING_FORM_FIELDS = "missing-form-fields";
public static final String SERVER_ERROR = "server-error";
}
127 changes: 127 additions & 0 deletions java/email-contact-form/src/Main.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package io.openruntimes.java.src;

import io.openruntimes.java.RuntimeContext;
import io.openruntimes.java.RuntimeOutput;
import java.util.Map;
import java.util.List;
import java.util.HashMap;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.Collections;
import java.net.URI;
import java.net.URISyntaxException;
import io.appwrite.Client;
import jakarta.mail.MessagingException;


public class Main {

public RuntimeOutput main(RuntimeContext context) throws Exception {

List<String> requiredEnvVariables = Arrays.asList(
"SUBMIT_EMAIL",
"SMTP_HOST",
"SMTP_USERNAME",
"SMTP_PASSWORD"
);

Utils.throwIfMissing(System.getenv(), requiredEnvVariables);
if (System.getenv("ALLOWED_ORIGINS").equals("*")) {
context.log("WARNING: Allowing requests from any origin - this is a security risk!");
}

if (context.getReq().getMethod().equals("GET")) {
return context.getRes().send(
Utils.getHtmlContent("index.html"),
200,
Map.of("content-type", "text/html")
);
}


if(!context.getReq().getHeaders().get("content-type").equals("application/x-www-form-urlencoded")){
context.error("Incorrect content type");
String referer = context.getReq().getHeaders().get("referer");
return context.getRes().redirect(
String.format("%s?code=%s", referer, ErrorCode.INVALID_REQUEST)
);
}

if(!Cors.isOriginPermitted(context)){
context.error("Origin not permitted");
String referer = context.getReq().getHeaders().get("referer");
return context.getRes().redirect(
String.format("%s?code=%s", referer, ErrorCode.INVALID_REQUEST)
);
}

Map<String, List<String>> formData = new HashMap<>();
String body = (String) context.getReq().getBody();
String[] params = body.split("&");
for (String param : params) {
String[] keyValue = param.split("=");
String key = keyValue[0];
String value = keyValue[1];
if (!formData.containsKey(key)) {
formData.put(key, new ArrayList<>());
}
formData.get(key).add(value);
}

Map<String, String> form = new HashMap<>();
for (Map.Entry<String, List<String>> entry : formData.entrySet()) {
form.put(entry.getKey(), entry.getValue().get(0));
}

try {
Utils.throwIfMissing(form, Collections.singletonList("email"));
} catch (IllegalArgumentException ex) {
String referer = context.getReq().getHeaders().get("referer");
return context.getRes().redirect(
String.format("%s?code=%s", referer, ErrorCode.MISSING_FORM_FIELDS),
301,
Cors.getCorsHeaders(context)
);
}


try {
Map<String, String> emailOptions = new HashMap<>();
emailOptions.put("from", System.getenv("SMTP_USERNAME"));
emailOptions.put("to", System.getenv("SUBMIT_EMAIL"));
emailOptions.put("subject", "New Contact Form Submission");
emailOptions.put("text", Utils.templateFormMessage(form));
Utils.sendEmail(emailOptions);
} catch (MessagingException ex) {
context.log("MessagingException: " + ex.getMessage());
String referer = context.getReq().getHeaders().get("referer");
return context.getRes().redirect(
String.format("%s?code=%s", referer, ErrorCode.SERVER_ERROR),
301,
Cors.getCorsHeaders(context)
);
} catch (Exception ex) {
context.log("Exception: " + ex.getMessage());
String referer = context.getReq().getHeaders().get("referer");
return context.getRes().redirect(
String.format("%s?code=%s", referer, ErrorCode.SERVER_ERROR),
301,
Cors.getCorsHeaders(context)
);
}

if (form.get("_next") == null || form.get("_next").isEmpty()) {
return context.getRes().send(
Utils.getHtmlContent("success.html"),
200,
Map.of("content-type", "text/html; charset=utf-8")
);
}

return context.getRes().redirect(
Utils.joinURL(context.getReq().getHeaders().get("referer"), form.get("_next").substring(0,1)),
301,
Cors.getCorsHeaders(context)
);
}
}
Loading