Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
e3020fa
test: add a recording HTTP client for tests
tomasmartty Oct 29, 2026
b4a354e
fix: throw MailerLiteException for network errors
tomasmartty Oct 29, 2026
2f66510
feat: add sendRequest for any HTTP method
tomasmartty Oct 29, 2026
ef982b2
feat: expose the API error code and etag on MailerLiteException
tomasmartty Oct 29, 2026
cdedfdd
fix: encode query values and skip null values
tomasmartty Oct 29, 2026
dc0b5f0
feat: add shared input checks and the editor content adapter
tomasmartty Oct 29, 2026
7c2d178
feat: add Emails.get
tomasmartty Oct 29, 2026
cec395b
feat: add Emails.getHtml
tomasmartty Oct 29, 2026
6a194eb
feat: add Emails.getBlock
tomasmartty Oct 29, 2026
52d14a3
feat: add Emails.createBlock
tomasmartty Oct 29, 2026
8b21926
feat: add Emails.updateBlock
tomasmartty Oct 29, 2026
9f4385c
feat: add Emails.deleteBlock
tomasmartty Oct 29, 2026
b29ebdf
feat: add Emails.moveBlock
tomasmartty Oct 29, 2026
62166e4
feat: add Emails.validateBlock
tomasmartty Oct 29, 2026
4672cd8
feat: add Emails.updateSettings
tomasmartty Oct 29, 2026
58f153f
feat: add Emails.validateSettings
tomasmartty Oct 29, 2026
64192ff
feat: add Emails.createTemplate
tomasmartty Oct 29, 2026
9c855f6
feat: add Emails.versions
tomasmartty Oct 29, 2026
3f26d7f
feat: add Emails.createVersion
tomasmartty Oct 29, 2026
e3d197f
feat: add Emails.restoreVersion
tomasmartty Oct 29, 2026
d5cf12d
feat: add BrandStyles.get
tomasmartty Oct 29, 2026
8ef1fe1
feat: add the files list
tomasmartty Oct 29, 2026
b3ddbce
feat: create campaigns with the simple editor
tomasmartty Oct 29, 2026
0a7e9f6
feat: add idempotency keys to email writes
tomasmartty Oct 29, 2026
f9c983f
chore: bump the version to 0.1.0
tomasmartty Oct 29, 2026
4acb5e2
fix: keep a message for network errors without a message
tomasmartty Oct 29, 2026
32c300f
test: lock the encoding of query values
tomasmartty Oct 29, 2026
56a0056
docs: add the changes to know before an upgrade to 0.1.0
tomasmartty Oct 29, 2026
ac14594
docs: correct what an empty string does in a settings update
tomasmartty Oct 29, 2026
55bd250
test: compare query parameters in any sequence
tomasmartty Oct 29, 2026
be4f2df
chore: remove redundant comments
tomasmartty Oct 29, 2026
42b00b2
refactor: leave value checks to the API
tomasmartty Oct 30, 2026
8682cab
fix: reject idempotency keys that Java cannot send
tomasmartty Oct 30, 2026
0c3fbfb
refactor: remove the exceptions-to-emails package cycle
tomasmartty Oct 30, 2026
f77dcb0
fix: set the message field of MailerLiteException
tomasmartty Oct 30, 2026
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
651 changes: 649 additions & 2 deletions README.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.mailerlite</groupId>
<artifactId>mailerlite-java</artifactId>
<version>0.0.4</version>
<version>0.1.0</version>
<name>MailerLite Java SDK</name>
<description>An SDK for MailerLite API</description>
<url>https://github.com/mailerlite/mailerlite-java</url>
Expand Down
57 changes: 57 additions & 0 deletions src/main/java/com/mailerlite/sdk/MailerLite.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,11 @@
package com.mailerlite.sdk;

import com.mailerlite.sdk.automations.Automations;
import com.mailerlite.sdk.brandstyles.BrandStyles;
import com.mailerlite.sdk.campaigns.Campaigns;
import com.mailerlite.sdk.emails.Emails;
import com.mailerlite.sdk.fields.Fields;
import com.mailerlite.sdk.files.MediaFiles;
import com.mailerlite.sdk.forms.Forms;
import com.mailerlite.sdk.groups.Groups;
import com.mailerlite.sdk.segments.Segments;
Expand Down Expand Up @@ -49,6 +52,15 @@ public class MailerLite
/** The forms. */
protected Forms forms;

/** The emails. */
protected Emails emails;

/** The brand styles. */
protected BrandStyles brandStyles;

/** The files of the media library. */
protected MediaFiles files;

/**
* Campaigns.
*
Expand Down Expand Up @@ -173,6 +185,51 @@ public Forms forms()
}


/**
* Emails built with the simple editor.
*
* @return the emails
*/
public Emails emails()
{
if (emails == null) {
emails = new Emails(this);
}

return emails;
}


/**
* Brand styles.
*
* @return the brand styles
*/
public BrandStyles brandStyles()
{
if (brandStyles == null) {
brandStyles = new BrandStyles(this);
}

return brandStyles;
}


/**
* Files of the media library.
*
* @return the files
*/
public MediaFiles files()
{
if (files == null) {
files = new MediaFiles(this);
}

return files;
}


/**
* Sets the MailerLite API token.
*
Expand Down
110 changes: 92 additions & 18 deletions src/main/java/com/mailerlite/sdk/MailerLiteApi.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import java.net.http.HttpResponse;
import java.net.http.HttpRequest.BodyPublishers;
import java.net.http.HttpResponse.BodyHandlers;
import java.util.Map;
import java.util.NoSuchElementException;

import com.google.gson.Gson;
Expand Down Expand Up @@ -100,9 +101,7 @@ public <T extends MailerLiteResponse> T getRequest(String endpoint, Class<T> res

} catch (IOException | InterruptedException e) {

MailerLiteException ex = (MailerLiteException) e;

throw ex;
throw this.networkError(e);
}

if (responseClass == MailerLiteStringResponse.class) {
Expand Down Expand Up @@ -161,9 +160,7 @@ public <T extends MailerLiteResponse> T postRequest(String endpoint, String requ

} catch (IOException | InterruptedException e) {

MailerLiteException ex = (MailerLiteException) e;

throw ex;
throw this.networkError(e);
}

if (responseClass == MailerLiteStringResponse.class) {
Expand Down Expand Up @@ -218,9 +215,7 @@ public <T extends MailerLiteResponse> T deleteRequest(String endpoint, Class<T>

} catch (IOException | InterruptedException e) {

MailerLiteException ex = (MailerLiteException) e;

throw ex;
throw this.networkError(e);
}

return this.handleApiResponse(responseObject, responseClass, customDeserializer);
Expand Down Expand Up @@ -274,9 +269,7 @@ public <T extends MailerLiteResponse> T putRequest(String endpoint, String reque

} catch (IOException | InterruptedException e) {

MailerLiteException ex = (MailerLiteException) e;

throw ex;
throw this.networkError(e);
}

if (responseClass == MailerLiteStringResponse.class) {
Expand All @@ -291,6 +284,79 @@ public <T extends MailerLiteResponse> T putRequest(String endpoint, String reque
}


/**
* Sends a request with any HTTP method, for example PATCH, to the given endpoint of the MailerLite API.
* <p>
* The request has the headers {@code Content-Type: application/json} and {@code Accept: application/json},
* and then the extra headers.
* <p>
* If the body is null, GET and DELETE requests have no body, and the other requests have an empty body.
* The SDK never sends the JSON {@code null}.
*
* @param <T> the generic type
* @param method the HTTP method, for example "PATCH"
* @param endpoint the MailerLite API endpoint, with the query string
* @param body the JSON body, or null for no body
* @param headers the extra headers, or null
* @param responseClass the class of the response object
* @return T
* @throws MailerLiteException if the API returns an error or the request fails
*/
public <T extends MailerLiteResponse> T sendRequest(String method, String endpoint, String body, Map<String, String> headers, Class<T> responseClass) throws MailerLiteException
{
HttpRequest.Builder builder = HttpRequest.newBuilder(URI.create(this.endpointBase.concat(endpoint)))
.header("Content-Type", "application/json")
.header("Accept", "application/json")
.header("Authorization", "Bearer ".concat(this.apiToken));

if (headers != null) {

for (Map.Entry<String, String> header : headers.entrySet()) {

builder.header(header.getKey(), header.getValue());
}
}

if (body != null) {

builder.method(method, BodyPublishers.ofString(body));
} else if ("GET".equals(method)) {

builder.GET();
} else if ("DELETE".equals(method)) {

builder.DELETE();
} else {

builder.method(method, BodyPublishers.noBody());
}

HttpResponse<String> responseObject = null;

try {

responseObject = this.client.send(builder.build(), BodyHandlers.ofString());
} catch (IOException | InterruptedException e) {

throw this.networkError(e);
}

return this.handleApiResponse(responseObject, responseClass, null);
}


/** Uses e.toString() as the message, because e.getMessage() can be null. */
private MailerLiteException networkError(Exception e)
{
if (e instanceof InterruptedException) {

Thread.currentThread().interrupt();
}

return new MailerLiteException(e.toString(), e);
}


/**
* Handles the response from the MailerLite API. It deserializes the JSON response into an object with the given type
*
Expand Down Expand Up @@ -340,9 +406,19 @@ private <T extends MailerLiteResponse> T handleApiResponse(HttpResponse<String>

JsonResponseError error = gson.fromJson(stringResponse, JsonResponseError.class);

responseError = new MailerLiteException(error.message);

String message = error.message;

if (error.code != null && !error.code.isEmpty()) {

message = error.code.concat(": ").concat(String.valueOf(error.message));
}

responseError = new MailerLiteException(message);

responseError.message = error.message == null ? "" : error.message;
responseError.errors = error.errors;
responseError.errorCode = error.code;
responseError.etag = error.etag;
} catch (Exception ex) {

responseError = new MailerLiteException("Error parsing API response");
Expand All @@ -366,11 +442,9 @@ private <T extends MailerLiteResponse> T handleApiResponse(HttpResponse<String>
try {

response = responseClass.getDeclaredConstructor().newInstance();
} catch (Exception e) {

MailerLiteException ex = (MailerLiteException) e;
} catch (ReflectiveOperationException e) {

throw ex;
throw new MailerLiteException(e.getMessage(), e);
}
}

Expand Down
30 changes: 30 additions & 0 deletions src/main/java/com/mailerlite/sdk/MailerLiteResponse.java
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,34 @@ public Boolean successful()
{
return responseStatusCode >= 200 && responseStatusCode < 300;
}

/**
* Tells if the API returned the stored response of an earlier request with the same idempotency key.
* <p>
* The SDK finds the header Idempotent-Replayed without case, and compares its value with "true" without case.
*
* @return true if the response is a replay
*/
public boolean isIdempotentReplay()
{
if (headers == null) {
return false;
}

for (Map.Entry<String, List<String>> header : headers.entrySet()) {

if (!"Idempotent-Replayed".equalsIgnoreCase(header.getKey()) || header.getValue() == null) {
continue;
}

for (String value : header.getValue()) {

if (value != null && "true".equalsIgnoreCase(value.trim())) {
return true;
}
}
}

return false;
}
}
55 changes: 55 additions & 0 deletions src/main/java/com/mailerlite/sdk/RequestOptions.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*************************************************
* MailerLite Java SDK
* https://github.com/mailerlite/mailerlite-java
*
* @author MailerLite <support@mailerlite.com>
* https://mailerlite.com
**************************************************/
package com.mailerlite.sdk;

/**
* Options for one request, for example an idempotency key.
* <p>
* The email writes, {@code Emails.createTemplate} and {@code Emails.createVersion} accept the options as the last
* argument.
*/
public final class RequestOptions {

private final String idempotencyKey;

private RequestOptions(String idempotencyKey)
{
this.idempotencyKey = idempotencyKey;
}

/**
* Makes options that send the key in the Idempotency-Key header.
* <p>
* Use a new key for each write, and the same key when you send that write again. If the first request was a
* success, the API returns the stored response and does not do the write two times. Then
* {@link MailerLiteResponse#isIdempotentReplay()} is true. The API keeps a key for 24 hours.
* <p>
* The SDK sends the key as you give it, and the API checks it. A null or empty key sends no header.
* <p>
* The SDK rejects a key with a character that is not printable ASCII (0x20 to 0x7E), because the Java HTTP client
* would send each character that is not ASCII as "?". Then the write method throws
* {@link com.mailerlite.sdk.exceptions.InvalidIdempotencyKeyException} and sends no request.
*
* @param key the idempotency key, or null for no key
* @return the options
*/
public static RequestOptions idempotencyKey(String key)
{
return new RequestOptions(key);
}

/**
* Gets the idempotency key.
*
* @return the idempotency key, or null
*/
public String getIdempotencyKey()
{
return idempotencyKey;
}
}
Loading