Microsoft Graph 실무 입문 #4 메일과 캘린더 자동화: 읽기, 보내기, 일정 생성
3편의 골격 위에 첫 실무 시나리오를 얹습니다. 메일과 캘린더는 Graph 자동화 수요에서 가장 큰 비중을 차지합니다. 매일 아침 보고서를 발송하고, 특정 메일함을 감시해 첨부를 수집하고, 회의를 자동으로 잡는 일들입니다. 이번 편의 예제는 무인 자동화를 전제로 application 권한 클라이언트를 쓰되, delegated로 바꿀 때는 users.by_user_id(...)를 me로 줄이면 된다는 3편의 대응 규칙이 그대로 적용됩니다.
먼저 권한부터 정리해 둡니다. 이번 편에서 등장하는 것은 메일 읽기 Mail.Read, 발송 Mail.Send, 캘린더 Calendars.ReadWrite, 가용성 조회 Calendars.Read(getSchedule은 Schedule.Read.All도 가능)입니다. application 권한이라면 전부 관리자 동의 대상이고, 2편에서 언급한 ApplicationAccessPolicy로 접근 가능한 메일함을 자동화 계정 몇 개로 좁혀 두는 것이 안전한 구성입니다.
수신함 읽기: 필터와 함께 #
메일 조회의 기본형입니다. “지난 24시간 안에 온, 제목에 [일일보고]가 붙은 메일"을 뽑아 봅니다.
from datetime import datetime, timedelta, timezone
from msgraph.generated.users.item.messages.messages_request_builder import (
MessagesRequestBuilder,
)
async def recent_reports(client, user_id: str):
since = (datetime.now(timezone.utc) - timedelta(days=1)).strftime(
"%Y-%m-%dT%H:%M:%SZ"
)
params = MessagesRequestBuilder.MessagesRequestBuilderGetQueryParameters(
filter=f"receivedDateTime ge {since} and startsWith(subject, '[일일보고]')",
select=["subject", "from", "receivedDateTime", "hasAttachments"],
orderby=["receivedDateTime desc"],
top=50,
)
config = MessagesRequestBuilder.MessagesRequestBuilderGetRequestConfiguration(
query_parameters=params,
)
result = await client.users.by_user_id(user_id).messages.get(
request_configuration=config
)
return result.value or []실무 요령 두 가지를 덧붙입니다. 첫째, 기본 조회 대상은 메일함 전체이므로 수신함만 보려면 mail_folders.by_mail_folder_id("inbox").messages처럼 폴더를 경유합니다. 둘째, 본문(body)은 크기가 커서 목록 단계에서는 $select에서 빼고, 선별한 메일만 개별 조회로 본문을 가져오는 2단계가 스로틀링과 속도 양쪽에 유리합니다.
첨부 수집도 한 단계만 더하면 됩니다. hasAttachments가 참인 메일에 대해 messages/{id}/attachments를 조회하면 첨부 목록이 오고, 일반 파일 첨부(fileAttachment)의 경우 contentBytes에 Base64로 내용이 담겨 있어 디코드해 저장하면 끝입니다.
메일 보내기: sendMail 한 번이면 됩니다 #
발송은 초안 생성 → 발송의 두 단계도 가능하지만, 실무 자동화의 99%는 한 번에 보내는 sendMail이면 충분합니다.
from msgraph.generated.models.message import Message
from msgraph.generated.models.item_body import ItemBody
from msgraph.generated.models.body_type import BodyType
from msgraph.generated.models.recipient import Recipient
from msgraph.generated.models.email_address import EmailAddress
from msgraph.generated.users.item.send_mail.send_mail_post_request_body import (
SendMailPostRequestBody,
)
def to_recipient(addr: str) -> Recipient:
return Recipient(email_address=EmailAddress(address=addr))
async def send_report(client, sender_id: str, to: list[str], html: str):
body = SendMailPostRequestBody(
message=Message(
subject="[자동발송] 주간 배포 현황",
body=ItemBody(content_type=BodyType.Html, content=html),
to_recipients=[to_recipient(a) for a in to],
),
save_to_sent_items=True,
)
await client.users.by_user_id(sender_id).send_mail.post(body)발신자(sender_id)는 application 권한에서는 임의 사용자를 지정할 수 있지만, 실무에서는 noreply@... 같은 전용 자동화 계정(또는 공유 메일박스)을 만들어 그 계정으로만 보내는 것이 정석입니다. 사람 계정으로 보내면 회신·부재가 뒤엉키고, 감사(audit) 관점에서도 자동 발송과 사람의 발송이 섞이는 것은 피해야 합니다. 첨부가 필요하면 message.attachments에 fileAttachment(이름 + Base64 내용)를 더하면 되고, 3MB를 넘는 큰 첨부는 5편에서 다룰 업로드 세션과 같은 방식의 별도 절차가 필요하다는 것만 기억해 둡니다.
대량 발송에는 주의가 필요합니다. Exchange 쪽에는 계정당 발송량 제한(대략 하루 1만 통 수준)과 수신자 수 제한이 있고, 수백 명에게 루프로 보내는 코드는 7편에서 다룰 스로틀링(429)을 가장 먼저 만나는 경우이기도 합니다. 뉴스레터급 대량 메일은 Graph가 아니라 전문 발송 서비스의 영역이라는 경계선도 함께 그어 둡니다.
일정 만들기: 참석자 초대까지 #
캘린더 쓰기의 기본형은 이벤트 생성입니다. 참석자를 넣으면 초대 메일까지 Exchange가 알아서 발송합니다.
from msgraph.generated.models.event import Event
from msgraph.generated.models.date_time_time_zone import DateTimeTimeZone
from msgraph.generated.models.attendee import Attendee
from msgraph.generated.models.attendee_type import AttendeeType
async def create_meeting(client, organizer_id: str):
event = Event(
subject="배포 회고",
start=DateTimeTimeZone(
date_time="2026-08-20T10:00:00", time_zone="Asia/Seoul"
),
end=DateTimeTimeZone(
date_time="2026-08-20T11:00:00", time_zone="Asia/Seoul"
),
attendees=[
Attendee(
email_address=EmailAddress(address="dev-team@contoso.com"),
type=AttendeeType.Required,
),
],
is_online_meeting=True, # Teams 회의 링크 자동 생성
online_meeting_provider="teamsForBusiness",
)
return await client.users.by_user_id(organizer_id).events.post(event)is_online_meeting=True 한 줄로 Teams 회의 링크가 붙는다는 것, 반복 일정은 recurrence 속성으로 표현한다는 것 정도가 자주 쓰는 확장입니다.
시간대: 캘린더 자동화의 단골 함정 #
캘린더 코드에서 가장 많은 버그가 시간대에서 나옵니다. 규칙 두 가지를 세워 두면 대부분 예방됩니다.
- 쓸 때는 위 예제처럼
dateTime + timeZone을 항상 쌍으로 명시합니다. “로컬 시간이겠지"라고 가정한 코드는 서버(UTC 기준)에서 돌아가는 순간 9시간이 밀립니다. - 읽을 때는 Graph가 기본적으로 UTC로 돌려준다는 것을 전제하거나, 요청 헤더
Prefer: outlook.timezone="Asia/Seoul"을 붙여 원하는 시간대로 변환된 값을 받습니다. 조회 결과를 그대로 사람에게 보여 주는 자동화(일일 일정 요약 등)라면 후자가 편합니다.
기간으로 일정을 뽑을 때는 events가 아니라 calendarView를 쓴다는 것도 함께 기억해 둡니다. calendarView?startDateTime=...&endDateTime=...은 반복 일정을 실제 발생 회차로 펼쳐서 돌려주므로, “이번 주 일정"류의 조회는 반드시 이쪽입니다. events는 반복 일정을 정의 1건으로만 돌려줘서 주간 요약에서 반복 회의가 통째로 빠지는 사고가 납니다.
빈 시간 찾기: getSchedule #
“참석자들이 다 되는 시간"을 찾는 자동화의 재료는 getSchedule입니다. 여러 사용자의 바쁨/한가함 정보를 한 번에 조회하고, 회의실도 메일 주소를 가진 리소스라서 같은 방법으로 조회됩니다.
from msgraph.generated.users.item.calendar.get_schedule.get_schedule_post_request_body import (
GetSchedulePostRequestBody,
)
async def check_availability(client, user_id: str, emails: list[str]):
body = GetSchedulePostRequestBody(
schedules=emails,
start_time=DateTimeTimeZone(
date_time="2026-08-20T09:00:00", time_zone="Asia/Seoul"
),
end_time=DateTimeTimeZone(
date_time="2026-08-20T18:00:00", time_zone="Asia/Seoul"
),
availability_view_interval=30, # 30분 단위
)
result = await client.users.by_user_id(user_id).calendar.get_schedule.post(body)
for sched in result.value:
# availability_view: "0"=한가 "1"=미정 "2"=바쁨 "3"=부재 의 문자열
print(sched.schedule_id, sched.availability_view)응답의 availabilityView는 지정한 간격(위에서는 30분) 단위의 상태를 숫자 문자열로 돌려줍니다. "002200..."처럼 오므로, 참석자 전원의 문자열에서 같은 위치가 모두 0인 구간을 찾으면 그것이 공통의 빈 시간입니다. 문자열 처리 몇 줄로 회의 시간 추천기가 만들어지는, 노력 대비 효과가 좋은 API입니다.
정리 #
- 권한은 Mail.Read / Mail.Send / Calendars.ReadWrite가 기본 세트이고, application이라면 ApplicationAccessPolicy로 접근 범위를 자동화 계정으로 좁혀 둡니다.
- 메일 목록은 폴더 경유 +
$select에서 본문 제외 + 2단계 조회가 기본기입니다. 발송은 sendMail 하나로 끝나고, 발신은 전용 자동화 계정으로 통일합니다. - 대량 발송은 Exchange의 발송 한도와 스로틀링의 지뢰밭입니다. 뉴스레터급은 Graph의 영역이 아닙니다.
- 일정은 dateTime+timeZone을 항상 쌍으로, 조회는
Prefer: outlook.timezone으로, 기간 조회는 events가 아니라 calendarView로 합니다. 반복 일정 관련 사고의 대부분이 이 세 가지에서 나옵니다. - getSchedule의 availabilityView 문자열을 겹치면 “전원이 되는 시간"이 나옵니다. 회의실도 같은 방법으로 조회됩니다.
다음 편은 파일입니다. OneDrive와 SharePoint의 구조(drive와 site), 대용량 업로드 세션, 공유 링크 생성을 다룹니다.