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 1 commit
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
PalaniappanC marked this conversation as resolved.
Show resolved Hide resolved

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 | `no-reply@mywebapp.org` |

### 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 | `me@mywebapp.org` |

### 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'
}
55 changes: 55 additions & 0 deletions java/email-contact-form/src/Cors.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
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;
import java.lang.*;
PalaniappanC marked this conversation as resolved.
Show resolved Hide resolved


public class Cors {

/*
PalaniappanC marked this conversation as resolved.
Show resolved Hide resolved
* Returns true if the origin is allowed to make requests to this endpoint
*
* Parameters:
* req: Request object
PalaniappanC marked this conversation as resolved.
Show resolved Hide resolved
*
* 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");
PalaniappanC marked this conversation as resolved.
Show resolved Hide resolved
return originHeader != null && allowedOriginsList.contains(originHeader);
}

/*
* Returns the CORS headers for the request
*
* Parameters:
* req: Request object
PalaniappanC marked this conversation as resolved.
Show resolved Hide resolved
*
* 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";
}
133 changes: 133 additions & 0 deletions java/email-contact-form/src/Main.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
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 java.lang.*;
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");
String errorCode = ErrorCode.INVALID_REQUEST;
return context.getRes().redirect(
String.format("%s?code=%s", referer, errorCode)
);
}

if(!Cors.isOriginPermitted(context)){
context.error("Origin not permitted");
String referer = context.getReq().getHeaders().get("referer");
String errorCode = ErrorCode.INVALID_REQUEST;
PalaniappanC marked this conversation as resolved.
Show resolved Hide resolved
return context.getRes().redirect(
String.format("%s?code=%s", referer, errorCode)
);
}

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");
String errorCode = ErrorCode.MISSING_FORM_FIELDS;
PalaniappanC marked this conversation as resolved.
Show resolved Hide resolved
return context.getRes().redirect(
String.format("%s?code=%s", referer, errorCode),
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");
String errorCode = ErrorCode.SERVER_ERROR;
PalaniappanC marked this conversation as resolved.
Show resolved Hide resolved
return context.getRes().redirect(
String.format("%s?code=%s", referer, errorCode),
301,
Cors.getCorsHeaders(context)
);
} catch (Exception ex) {
context.log("Exception: " + ex.getMessage());
String referer = context.getReq().getHeaders().get("referer");
String errorCode = ErrorCode.SERVER_ERROR;
PalaniappanC marked this conversation as resolved.
Show resolved Hide resolved
return context.getRes().redirect(
String.format("%s?code=%s", referer, errorCode),
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