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

CORS problem when using /api/view #434

Open
baiomys opened this issue Feb 1, 2025 · 19 comments
Open

CORS problem when using /api/view #434

baiomys opened this issue Feb 1, 2025 · 19 comments

Comments

@baiomys
Copy link

baiomys commented Feb 1, 2025

HI.

Is it possible to preprocess HTML content to do JS analog of

const links = document.getElementById('doc').contentWindow.document.getElementsByTagName('a');
 for (let link of links) {
   link.setAttribute('target', '_blank'); 
 }

for external links ?

When showing HTML content in IFRAME external links fail if not preprocessed, but it's not an option when using cross-domain IFRAME. Prohibited by CORS.

Thanks.

@axllent
Copy link
Owner

axllent commented Feb 2, 2025

Hi. Did you set the the --api-cors "*" flag (MP_API_CORS="*" env) in Mailpit?

@axllent
Copy link
Owner

axllent commented Feb 2, 2025

Sorry, I just realised that the CORS policy only applies to the /api/* route, and not the /view/*.html route, so that won't currently help.

I prefer not to hardcode target="_blank" in the HTML version as that may impact other existing integrations and/or tests. I believe that if I fix the CORS to also include the /view/*.html route too, then your JavaScript should be allowed to run.

@baiomys
Copy link
Author

baiomys commented Feb 2, 2025

Thanks, currently Caddy routes all api calls to/from several mailpits and CORS problem can be solved by complicated redirects,
but it is kind of 'dirty hack' I prefer no to use.

@axllent
Copy link
Owner

axllent commented Feb 2, 2025

Does that mean that it will work for you if I apply (fix) the CORS policy to the html route?

@baiomys
Copy link
Author

baiomys commented Feb 2, 2025

IMHO fixing CORS issue will help to prevent further questions from other users.
You program is getting popular.
=)

@baiomys
Copy link
Author

baiomys commented Feb 2, 2025

Alternatively you can use endpoint already covered by CORS

/api/v1/message/{ID}/part/{PartID}

using part id = html for example to return html

It will also reduce number of routes to handle on reverse-proxy side.

@axllent
Copy link
Owner

axllent commented Feb 2, 2025

I have pushed a change for this to the axllent/mailpit:edge docker image (I haven't released it yet). Would you be able to test that to see if it solves your issue please? Thanks.

@baiomys
Copy link
Author

baiomys commented Feb 2, 2025

No luck
Uncaught SecurityError: Failed to read a named property 'document' from 'Window': Blocked from accessing a cross-origin frame.
It seems that Access-Control-Allow-Origin does not affect IFRAME access as expected
https://stackoverflow.com/questions/23362842/access-control-allow-origin-not-working-for-iframe-within-the-same-domain

Also tried to manually inject Access-Control-Allow-Origin in caddy response. Same result.

Thanks for trying.

@axllent
Copy link
Owner

axllent commented Feb 2, 2025

Hmmm, that is unfortunate. OK, what do you think would solve this issue for you? Is it just the target="_blank", or is there more to it? You are able to load the iframe from your application, however the links currently just open within the iframe, am I correct?

@baiomys
Copy link
Author

baiomys commented Feb 2, 2025

IMHO hardcoding target="_blank" can be helpful not only in my case.
And I am not asking to do something exceptional just for me and right away.

This IFRAME dilemma is obvious and can arise any time soon.

To preserve current API structure, solution can be bound to existing /view or /api/v1/message/{ID}/part/{PartID} endpoint using new context.

@axllent
Copy link
Owner

axllent commented Feb 2, 2025

I've done a bit more reading, and it is a browser-based security restriction to prevent hacking (you won;t be able to work around it via JavaScript). For instance, if I embedded the Gmail login page within an iframe on my malicious site, and then used JavaScript to listen on the input, and submit the data elsewhere as you log in.

  1. So I do not believe JavaScript is an option here if you are running the frontend and Mailpit API on different domains.
  2. I am still reluctant to modify the default "preview HTML" as-is because of backwards compatibility.

If I was to manipulate the HTML on the Mailpit end, I'm not entirely sure what the best way is. The reason is that while there are DOM-parsing libraries there, they always manipulate more than just the anchor tags, they also create <head> & <body> tags (if they don't already exist), and potentially "fix" broken HTML. This may not be bad in your case, but it would definitely affect user testing.

So, I could potentially add a URL variable option, so something like /view/<id>.html?embed=1 or something, which then could do the manipulation, and we don't care if it adds or modifies HTML code. There are some big risks though with embedding as an iframe which you should be aware of, for instance if the HTML email contains inline JavaScript, but that is a different discussion.

The other option is that your application reads the HTML message data from the AP, and then injects it into your page. I do something similar already in Mailpit itself, injecting the data into a blank iframe using srcdoc instead of src. This gives you far more control, however does require several HTML manipulation processes (converting embedded image paths, and I also sanitize the HTML to remove javascript etc). Most of this could be copied from Mailpit's code (although it is in Vue). This is the best way I think, but requires more work on your end. It does however allow you more control to change things like the iframe's height, and opening links in new windows etc.

@baiomys
Copy link
Author

baiomys commented Feb 2, 2025

mailpit instance is always self hosted, so you can relatively easy attach regex/sed query via api request parameter and DON'T CARE about result. End user should care.

srcdoc is not an option. It can be implemented only using Caddy templates, which capabilities are too limited.
I cannot afford to process all requests on single core point without distributing among pits.

@axllent
Copy link
Owner

axllent commented Feb 2, 2025

You are embedding the html in an iframe, right? How are you dealing with things like iframe height and styling?

Just so I understand 100% - is adding target="_blank" the only change you need here to allow your application to preview the HTML message?

@baiomys
Copy link
Author

baiomys commented Feb 2, 2025

Well, it's definitely not a secret.
Currently I am using Jinja template.

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script>
// cookie with JWT containing ONLY expiration timestamp
document.cookie = "usess={{ jwt }}; domain={{ domain }}; path=/";

function unhide() {
  document.getElementById('back').style.background = 'none';
  document.getElementById('glass').style.display = 'block';
}

function show() {
  unhide();
  const links = document.getElementById('glass').contentWindow.document.getElementsByTagName('a');
  for (let link of links) {
    link.setAttribute('target', '_blank'); 
  }
}

setTimeout(() => { unhide() }, 2000);
</script>

<style>
        body { background: radial-gradient(#eee 50%, #999) fixed  !important;  }

        @media only screen and (max-width: 800px) {
            .somepad { padding: 0%; }
            .cntr { display: none; }
            .screen:before { height: 96vh }
        }

        @media only screen and (min-width: 800px) {
            .somepad { padding: 4%;  }
            .cntr { display:inline-block; }
            .screen:before {
            padding-top: 140%;
            }
        }

        .somepad {  margin: 0 auto; max-width: 1024px;  }

        .screen {
            background: radial-gradient(#000c04 70%, #c6c6c6);
            box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 0 0 2px rgba(0, 0, 0, 0.4) inset;
            border-radius: 6% / 4%;
            overflow: hidden;
            position: relative;
            width: 100%;
        }

        .screen:before {
            content: "";
            display: block;
            background: #F5F6F8;
            border-radius: 5%/3.5%;
            margin: .7%;
        }

        .screen:after {
            background: #2c3449;
            border-radius: 50%;
            box-shadow: 0 1px 1px 0 #fff, 0 1px 1px #000 inset;
            content: "";
            position: absolute;
            top: 4.5%;
            left: 49.2%;
            padding-top: 1.6%;
            width: 1.6%;
        }

        .cntr {
           position: absolute;
           top: 4.5%;
           left: 6%;
           width: 6.4%;
           color:rgb(0, 0, 0);
           text-align: left;
        }

        .button {
            /*background: #fff;*/
            background: #555;
            border-radius: 50%;
            bottom: 2.6%;
            box-shadow: 0 0 1px 0 #fff, 0 8px 7px rgba(0, 0, 0, 0.08) inset;
            height: 0;
            left: 47%;
            padding-top: 6.4%;
            position: absolute;
            width: 6.4%;
        }

        .button:after {
            background: #555;
            border-radius: 25%;
            box-shadow: 1px 1px 0 0 rgba(255, 255, 255, 0.7) inset, -1px -1px 0 0 rgba(255, 255, 255, 0.5) inset, 0 2px 5px rgba(0, 0, 0, 0.1) inset, -1px 0 8px rgba(0, 0, 0, 0.2), 1px 1px 1px rgba(0, 0, 0, 0.1);
            content: "";
            display: block;
            left: 35%;
            padding-top: 30%;
            position: absolute;
            top: 38%;
            width: 30%;
        }

        .viewport {
            position: absolute;
            bottom: 0;
            left: 0;
            right: 0;
            top: 0;
            margin: 13% 6%;
            border: 1px solid rgba(0, 0, 0, .2);
            background:url("https://cdnjs.cloudflare.com/ajax/libs/galleriffic/2.0.1/css/loader.gif") center no-repeat;
        }

        .content {
            height: 100%;
            width: 100%;
            border: none;
            display:none;
            background: #fff;
        }
    </style>
</head>

<body>
    <div class="somepad">
        <div class="screen">
            <span class="cntr">{{ hits }}</span>
            <div class="viewport" id="back">
                <iframe class="content" id="glass" src="{{ url }}" onload="show()"></iframe>
            </div>
            <div id="bada-boom" class="button"></div>
        </div>
    </div>
</body>
</html>

@axllent
Copy link
Owner

axllent commented Feb 2, 2025

Thanks, but what about my second question of my last message?

@baiomys
Copy link
Author

baiomys commented Feb 2, 2025

All is working in pre-alpha stage.

https://t.me/org_mailbot

Feel free to test if you use telegram.

Currently there is only one testing pit, so CORS is not a problem for now, but there will be more soon.
So if you kindly agree to fix this nasty issue it'll be great.

@axllent
Copy link
Owner

axllent commented Feb 2, 2025

No sorry, I don't have Telegram. Ok, let me think about this a little bit more. There is actually a way an embedded page can call a function (if it exists) in a parent page, which may be the better solution. Sure, you need just the target set, however others may require other functionality too. I've actually done this before in another project, so I'll need to do a bit of digging to see how that worked, and what the limitations (if any) were. At this stage I'm most concerned about security, and also flexibility.

@baiomys
Copy link
Author

baiomys commented Feb 2, 2025

Glad to hear that you are planning major improvements in project.

The most obvious way is to encode <script> section containing payload using base64, then pass it to mailpit in env variable and inject in HTML on api call to /view. Sure some examples will be handy.

It can be used directly src="data:text/javascript;base64,Y29uc29sZSgnSGVsbG8sIFdvcmxkIScpOw=="

@baiomys
Copy link
Author

baiomys commented Feb 2, 2025

AI generated example using messages
Not sure it's 100% correct, but concept looks usable.

<!DOCTYPE html>
<head>
    <title>Parent Window</title>
    <script>
        function sendCode() {
            const iframe = document.getElementById('myIframe');
            const code = "console.log('Hello from the iframe!');"; // Code to send
            iframe.contentWindow.postMessage(code, '*'); // Send code to iframe
        }
    </script>
</head>
<body>
    <h1>Parent Window</h1>
    <button onclick="sendCode()">Send Code to Iframe</button>
    <iframe id="myIframe" src="iframe.html" style="width: 100%; height: 200px;"></iframe>
</body>
</html>
<!DOCTYPE html>
<head>
    <title>Iframe</title>
    <script>
        window.addEventListener('message', function(event) {
            // Optionally, check the origin of the message for security
            // if (event.origin !== "http://your-expected-origin.com") return;

            const code = event.data; // Get the code from the message
            try {
                eval(code); // Execute the received code
            } catch (e) {
                console.error('Error executing code:', e);
            }
        });
    </script>
</head>
<body>
    <h1>Iframe</h1>
</body>
</html>


Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants