├── .gitignore ├── README.md ├── calendar.go ├── errors.go ├── event.go ├── folder.go ├── message.go ├── models.go ├── outlook.go ├── session.go └── utils.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | .vscode/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-outlook 2 | 3 | An SDK for accessing Microsoft's graph API with Go. 4 | 5 | I noticed while working on a project that there wasn't really any go sdk for outlook's api and so I decided to write this one. As of now, this package supports access to microsoft's graph api, exposing services for listing a user's calendars, their email folders, as well as exposing CRUD operations on calendar events and email messages. Also, as of right now, this project includes 0 dependencies. 6 | 7 | Unfortunately, Microsoft's graph API exposes a lot more than what I currently support in this SDK, so this will very much be a work in progress. Also, for the time being, I have decided not to implement Microsoft's authentication flow in this package. However, the session service does require a user's refreshToken and will automatically handle fetching the access token for the given refreshToken. 8 | 9 | ## Installation 10 | 11 | This is just a simple go package, so feel free to install via your tool of choice. 12 | 13 | ```bash 14 | go get https://github.com/amhester/go-outlook 15 | ``` 16 | 17 | ### Dependencies 18 | 19 | None 20 | 21 | ### Environment Variables 22 | 23 | This SDK requires the use of an authenticated session for all of it's exposed methods. Thus, it needs to be able to handle making requests on behalf of an application/user. To do that, the initial outlook client can be configured with both an App ID as well as an App Secret (provided by microsoft upon creation of an appliaction for their APIs). You can set these fields on the client either by passing them in as a ClientOpt on creation of the client, setting them after the client has been created, or through the following environment variables: 24 | 25 | ```bash 26 | OUTLOOK_APP_ID= 27 | OUTLOOK_APP_SECRET= 28 | ``` 29 | 30 | ## Usage 31 | 32 | Docs and Examples to come 33 | 34 | ## TODO 35 | 36 | Write TODOs 37 | 38 | ## Testing 39 | 40 | Yeah, probably still need to write some of those 41 | -------------------------------------------------------------------------------- /calendar.go: -------------------------------------------------------------------------------- 1 | package outlook 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | ) 7 | 8 | // CalendarService manages communication with microsofts graph for calendar resources. 9 | type CalendarService struct { 10 | session *Session 11 | basePath string 12 | } 13 | 14 | // NewCalendarService returns a new instance of a CalendarService. 15 | func NewCalendarService(session *Session) *CalendarService { 16 | return &CalendarService{ 17 | session: session, 18 | basePath: "/calendars", 19 | } 20 | } 21 | 22 | // CalendarListCall struct allowing for fluent style configuration of calls to the calendar list endpoint. 23 | type CalendarListCall struct { 24 | service *CalendarService 25 | nextLink string 26 | maxResults int64 27 | } 28 | 29 | // List returns a CalendarListCall builder struct 30 | func (cs *CalendarService) List() *CalendarListCall { 31 | return &CalendarListCall{ 32 | service: cs, 33 | maxResults: 10, 34 | } 35 | } 36 | 37 | // MaxResults sets the $top query parameter for the calendar list call. 38 | func (clc *CalendarListCall) MaxResults(pageSize int64) *CalendarListCall { 39 | clc.maxResults = pageSize 40 | return clc 41 | } 42 | 43 | // NextLink uses the link provided to set the $skip query parameter for the calendar list call. 44 | func (clc *CalendarListCall) NextLink(link string) *CalendarListCall { 45 | clc.nextLink = link 46 | return clc 47 | } 48 | 49 | // Do executes the calendar list call, returning the calendar list result. 50 | func (clc *CalendarListCall) Do(ctx context.Context) (*CalendarListResult, error) { 51 | params := map[string]interface{}{ 52 | "$top": clc.maxResults, 53 | "$count": true, 54 | } 55 | if clc.nextLink != "" { 56 | params["$skip"] = parsePageLink(clc.nextLink, "$skip") 57 | } 58 | 59 | var result CalendarListResult 60 | if _, err := clc.service.session.Get(ctx, clc.service.basePath, params, &result); err != nil { 61 | return nil, err 62 | } 63 | 64 | return &result, nil 65 | } 66 | 67 | // CalendarGetCall struct allowing for fluent style configuration of calls to the calendar get endpoint. 68 | type CalendarGetCall struct { 69 | service *CalendarService 70 | calendarID string 71 | } 72 | 73 | // Get returns an instance of a CalendarGetCall with the given calendarID. 74 | func (cs *CalendarService) Get(calendarID string) *CalendarGetCall { 75 | return &CalendarGetCall{ 76 | service: cs, 77 | calendarID: calendarID, 78 | } 79 | } 80 | 81 | // Do executes the http get request to microsoft's graph api to get the call's calendar. 82 | func (cgc *CalendarGetCall) Do(ctx context.Context) (*Calendar, error) { 83 | path := fmt.Sprintf("%s/%s", cgc.service.basePath, cgc.calendarID) 84 | calendar := Calendar{} 85 | if _, err := cgc.service.session.Get(ctx, path, nil, &calendar); err != nil { 86 | return nil, err 87 | } 88 | return &calendar, nil 89 | } 90 | 91 | // CalendarCreateCall struct allowing for fluent style configuration of calls to the calendar create endpoint. 92 | type CalendarCreateCall struct { 93 | service *CalendarService 94 | calendar *Calendar 95 | } 96 | 97 | // Create returns an instance of a CalendarCreateCall. 98 | func (cs *CalendarService) Create() *CalendarCreateCall { 99 | return &CalendarCreateCall{ 100 | service: cs, 101 | calendar: &Calendar{}, 102 | } 103 | } 104 | 105 | // Calendar sets the calendar data to be created on the call. 106 | func (ccc *CalendarCreateCall) Calendar(calendar *Calendar) *CalendarCreateCall { 107 | ccc.calendar = calendar 108 | return ccc 109 | } 110 | 111 | // Do executes the http post request to microsoft's graph api to create the call's calendar. 112 | func (ccc *CalendarCreateCall) Do(ctx context.Context) (*Calendar, error) { 113 | if _, err := ccc.service.session.Post(ctx, ccc.service.basePath, ccc.calendar, ccc.calendar); err != nil { 114 | return nil, err 115 | } 116 | return ccc.calendar, nil 117 | } 118 | 119 | // CalendarUpdateCall struct allowing for fluent style configuration of calls to the calendar update endpoint. 120 | type CalendarUpdateCall struct { 121 | service *CalendarService 122 | calendarID string 123 | calendar *Calendar 124 | } 125 | 126 | // Update returns an instance of a CalendarUpdateCall with the given calendarID. 127 | func (cs *CalendarService) Update(calendarID string) *CalendarUpdateCall { 128 | return &CalendarUpdateCall{ 129 | service: cs, 130 | calendarID: calendarID, 131 | calendar: &Calendar{}, 132 | } 133 | } 134 | 135 | // Calendar sets the calendar for the call. 136 | func (cuc *CalendarUpdateCall) Calendar(calendar *Calendar) *CalendarUpdateCall { 137 | cuc.calendar = calendar 138 | return cuc 139 | } 140 | 141 | // Do executes the http patch request to microsoft's graph api to update the call's calendar. 142 | func (cuc *CalendarUpdateCall) Do(ctx context.Context) (*Calendar, error) { 143 | path := fmt.Sprintf("%s/%s", cuc.service.basePath, cuc.calendarID) 144 | if _, err := cuc.service.session.Patch(ctx, path, cuc.calendar, cuc.calendar); err != nil { 145 | return nil, err 146 | } 147 | return cuc.calendar, nil 148 | } 149 | 150 | // CalendarDeleteCall struct allowing for fluent style configuration of calls to the calendar delete endpoint. 151 | type CalendarDeleteCall struct { 152 | service *CalendarService 153 | calendarID string 154 | } 155 | 156 | // Delete returns an instance of a CalendarDeleteCall with the given calendarID. 157 | func (cs *CalendarService) Delete(calendarID string) *CalendarDeleteCall { 158 | return &CalendarDeleteCall{ 159 | service: cs, 160 | calendarID: calendarID, 161 | } 162 | } 163 | 164 | // Do executes the http delete request to microsoft's graph api to delete the call's calendar. 165 | func (cdc *CalendarDeleteCall) Do(ctx context.Context) error { 166 | path := fmt.Sprintf("%s/%s", cdc.service.basePath, cdc.calendarID) 167 | if _, err := cdc.service.session.Delete(ctx, path, nil, nil); err != nil { 168 | return err 169 | } 170 | return nil 171 | } 172 | -------------------------------------------------------------------------------- /errors.go: -------------------------------------------------------------------------------- 1 | package outlook 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | var ( 9 | // ErrNoAccessToken is returned when a query is executed in a session which was either not given a refreshToken or that failed to retrieve and the access token. 10 | ErrNoAccessToken = fmt.Errorf("no access token for session") 11 | ) 12 | 13 | // ErrStatusCode an error thrown when a given http call responds with a bad http status 14 | type ErrStatusCode struct { 15 | Code int 16 | Message string 17 | SuggestedRetryDuration time.Duration 18 | } 19 | 20 | func (sce *ErrStatusCode) Error() string { 21 | return fmt.Sprintf( 22 | "Call to microsoft's graph api failed with a status code: %d. Reason: %s", 23 | sce.Code, 24 | sce.Message, 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /event.go: -------------------------------------------------------------------------------- 1 | package outlook 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | "time" 8 | ) 9 | 10 | var ( 11 | // DefaultEventFields the default set of fields that will be requested from microsoft's graph api when fecthing events 12 | DefaultEventFields = strings.Join([]string{ 13 | "id", 14 | "start", 15 | "end", 16 | "createdDateTime", 17 | "lastModifiedDateTime", 18 | "iCalUId", 19 | "subject", 20 | "isAllDay", 21 | "isCancelled", 22 | "isOrganizer", 23 | "showAs", 24 | "onlineMeetingUrl", 25 | "recurrence", 26 | "responseStatus", 27 | "location", 28 | "attendees", 29 | "organizer", 30 | "categories", 31 | "seriesMasterId", 32 | }, ",") 33 | ) 34 | 35 | // EventService manages communication with microsofts graph for event resources. 36 | type EventService struct { 37 | session *Session 38 | basePath string 39 | } 40 | 41 | // NewEventService returns a new instance of a EventService. 42 | func NewEventService(session *Session) *EventService { 43 | return &EventService{ 44 | session: session, 45 | basePath: "/events", 46 | } 47 | } 48 | 49 | // EventListCall struct allowing for fluent style configuration of calls to the event list endpoint. 50 | type EventListCall struct { 51 | service *EventService 52 | calendarID string 53 | nextLink string 54 | maxResults int64 55 | startTime time.Time 56 | endTime time.Time 57 | } 58 | 59 | // List returns a EventListCall struct 60 | func (es *EventService) List(calendarID string) *EventListCall { 61 | return &EventListCall{ 62 | service: es, 63 | maxResults: 10, 64 | calendarID: calendarID, 65 | } 66 | } 67 | 68 | // MaxResults sets the $top query parameter for the event list call. 69 | func (elc *EventListCall) MaxResults(pageSize int64) *EventListCall { 70 | elc.maxResults = pageSize 71 | return elc 72 | } 73 | 74 | // NextLink uses the link provided to set the $skip query parameter for the event list call. 75 | func (elc *EventListCall) NextLink(link string) *EventListCall { 76 | elc.nextLink = link 77 | return elc 78 | } 79 | 80 | // StartTime sets the startDateTime query parameter for the event list call. 81 | func (elc *EventListCall) StartTime(start time.Time) *EventListCall { 82 | elc.startTime = start 83 | return elc 84 | } 85 | 86 | // EndTime sets the endDateTime query parameter for the event list call. 87 | func (elc *EventListCall) EndTime(end time.Time) *EventListCall { 88 | elc.endTime = end 89 | return elc 90 | } 91 | 92 | // Do executes the event list call, returning the event list result. 93 | func (elc *EventListCall) Do(ctx context.Context) (*EventListResult, error) { 94 | params := map[string]interface{}{ 95 | "$top": elc.maxResults, 96 | "$count": true, 97 | "startDateTime": elc.startTime.Format(DefaultQueryDateTimeFormat), 98 | "endDateTime": elc.endTime.Format(DefaultQueryDateTimeFormat), 99 | "$select": DefaultEventFields, 100 | } 101 | if elc.nextLink != "" { 102 | params["$skip"] = parsePageLink(elc.nextLink, "$skip") 103 | } 104 | 105 | var path string 106 | if elc.calendarID == "primary" { 107 | path = "/calendarView" 108 | } else { 109 | path = fmt.Sprintf("/calendars/%s%s", elc.calendarID, "/calendarView") 110 | } 111 | 112 | var result EventListResult 113 | if _, err := elc.service.session.Get(ctx, path, params, &result); err != nil { 114 | return nil, err 115 | } 116 | 117 | return &result, nil 118 | } 119 | 120 | // EventGetCall struct allowing for fluent style configuration of calls to the event get endpoint. 121 | type EventGetCall struct { 122 | service *EventService 123 | calendarID string 124 | eventID string 125 | } 126 | 127 | // Get returns an instance of an EventGetCall with the given calendarID and eventID. 128 | func (es *EventService) Get(calendarID string, eventID string) *EventGetCall { 129 | return &EventGetCall{ 130 | service: es, 131 | calendarID: calendarID, 132 | eventID: eventID, 133 | } 134 | } 135 | 136 | // Do executes the http get to microsoft's graph api to get the call's event. 137 | func (egc *EventGetCall) Do(ctx context.Context) (*Event, error) { 138 | var path string 139 | if egc.calendarID == "primary" { 140 | path = fmt.Sprintf("/events/%s", egc.eventID) 141 | } else { 142 | path = fmt.Sprintf("/calendars/%s%s/%s", egc.calendarID, egc.service.basePath, egc.eventID) 143 | } 144 | event := Event{} 145 | if _, err := egc.service.session.Get(ctx, path, nil, &event); err != nil { 146 | return nil, err 147 | } 148 | return &event, nil 149 | } 150 | 151 | // EventCreateCall struct allowing for fluent style configuration of calls to the event create endpoint. 152 | type EventCreateCall struct { 153 | service *EventService 154 | calendarID string 155 | event *Event 156 | } 157 | 158 | // Create returns an instance of en EventCreateCall with the given calendarID. 159 | func (es *EventService) Create(calendarID string) *EventCreateCall { 160 | return &EventCreateCall{ 161 | service: es, 162 | calendarID: calendarID, 163 | event: &Event{}, 164 | } 165 | } 166 | 167 | // Event sets the event on the EventCreateCall. 168 | func (ecc *EventCreateCall) Event(event *Event) *EventCreateCall { 169 | ecc.event = event 170 | return ecc 171 | } 172 | 173 | // Do executes the http post to microsoft's graph api to create the call's event. 174 | func (ecc *EventCreateCall) Do(ctx context.Context) (*Event, error) { 175 | path := fmt.Sprintf("/calendars/%s%s", ecc.calendarID, ecc.service.basePath) 176 | if _, err := ecc.service.session.Post(ctx, path, ecc.event, ecc.event); err != nil { 177 | return nil, err 178 | } 179 | return ecc.event, nil 180 | } 181 | 182 | // EventUpdateCall struct allowing for fluent style configuration of calls to the event update endpoint. 183 | type EventUpdateCall struct { 184 | service *EventService 185 | calendarID string 186 | event *Event 187 | } 188 | 189 | // Update returns an instance of an EventUpdateCall with the given calendarID. 190 | func (es *EventService) Update(calendarID string) *EventUpdateCall { 191 | return &EventUpdateCall{ 192 | service: es, 193 | calendarID: calendarID, 194 | event: &Event{}, 195 | } 196 | } 197 | 198 | // Event sets the event on the EventUpdateCall. 199 | func (euc *EventUpdateCall) Event(event *Event) *EventUpdateCall { 200 | euc.event = event 201 | return euc 202 | } 203 | 204 | // Do executes the http patch to microsoft's graph api to update the call's event. 205 | func (euc *EventUpdateCall) Do(ctx context.Context) (*Event, error) { 206 | var path string 207 | if euc.calendarID == "primary" { 208 | path = fmt.Sprintf("/events/%s", euc.event.ID) 209 | } else { 210 | path = fmt.Sprintf("/calendars/%s%s/%s", euc.calendarID, euc.service.basePath, euc.event.ID) 211 | } 212 | if _, err := euc.service.session.Patch(ctx, path, euc.event, euc.event); err != nil { 213 | return nil, err 214 | } 215 | return euc.event, nil 216 | } 217 | 218 | // EventDeleteCall struct allowing for fluent style configuration of calls to the event delete endpoint. 219 | type EventDeleteCall struct { 220 | service *EventService 221 | calendarID string 222 | eventID string 223 | } 224 | 225 | // Delete returns an instance of an EventDeleteCall with the given calendarID and eventID. 226 | func (es *EventService) Delete(calendarID, eventID string) *EventDeleteCall { 227 | return &EventDeleteCall{ 228 | service: es, 229 | calendarID: calendarID, 230 | eventID: eventID, 231 | } 232 | } 233 | 234 | // Do executes the http delete to microsoft's graph api to delete the call's event. 235 | func (edc *EventDeleteCall) Do(ctx context.Context) error { 236 | var path string 237 | if edc.calendarID == "primary" { 238 | path = fmt.Sprintf("/events/%s", edc.eventID) 239 | } else { 240 | path = fmt.Sprintf("/calendars/%s%s/%s", edc.calendarID, edc.service.basePath, edc.eventID) 241 | } 242 | if _, err := edc.service.session.Delete(ctx, path, nil, nil); err != nil { 243 | return err 244 | } 245 | return nil 246 | } 247 | -------------------------------------------------------------------------------- /folder.go: -------------------------------------------------------------------------------- 1 | package outlook 2 | 3 | import "context" 4 | 5 | // FolderService manages communication with microsofts graph for folder resources. 6 | type FolderService struct { 7 | session *Session 8 | basePath string 9 | } 10 | 11 | // NewFolderService returns a new instance of a FolderService. 12 | func NewFolderService(session *Session) *FolderService { 13 | return &FolderService{ 14 | session: session, 15 | basePath: "/mailFolders", 16 | } 17 | } 18 | 19 | // FolderListCall struct allowing for fluent style configuration of calls to the mailFolder list endpoint. 20 | type FolderListCall struct { 21 | service *FolderService 22 | nextLink string 23 | maxResults int64 24 | } 25 | 26 | // List returns a FolderListCall builder struct 27 | func (fs *FolderService) List() *FolderListCall { 28 | return &FolderListCall{ 29 | service: fs, 30 | maxResults: 10, 31 | } 32 | } 33 | 34 | // MaxResults sets the $top query parameter for the folder list call. 35 | func (flc *FolderListCall) MaxResults(pageSize int64) *FolderListCall { 36 | flc.maxResults = pageSize 37 | return flc 38 | } 39 | 40 | // NextLink uses the link provided to set the $skip query parameter for the folder list call. 41 | func (flc *FolderListCall) NextLink(link string) *FolderListCall { 42 | flc.nextLink = link 43 | return flc 44 | } 45 | 46 | // Do executes the folder list call, returning the folder list result. 47 | func (flc *FolderListCall) Do(ctx context.Context) (*FolderListResult, error) { 48 | params := map[string]interface{}{ 49 | "$top": flc.maxResults, 50 | "$count": true, 51 | } 52 | if flc.nextLink != "" { 53 | params["$skip"] = parsePageLink(flc.nextLink, "$skip") 54 | } 55 | 56 | var result FolderListResult 57 | if _, err := flc.service.session.Get(ctx, flc.service.basePath, params, &result); err != nil { 58 | return nil, err 59 | } 60 | 61 | return &result, nil 62 | } 63 | -------------------------------------------------------------------------------- /message.go: -------------------------------------------------------------------------------- 1 | package outlook 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | ) 8 | 9 | // MessageService manages communication with microsofts graph for message resources. 10 | type MessageService struct { 11 | session *Session 12 | basePath string 13 | } 14 | 15 | // NewMessageService returns a new instance of a MessageService. 16 | func NewMessageService(session *Session) *MessageService { 17 | return &MessageService{ 18 | session: session, 19 | basePath: "/messages", 20 | } 21 | } 22 | 23 | // MessageListCall struct allowing for fluent style configuration of calls to the message list endpoint. 24 | type MessageListCall struct { 25 | service *MessageService 26 | folderID string 27 | nextLink string 28 | maxResults int64 29 | startTime time.Time 30 | endTime time.Time 31 | } 32 | 33 | // List returns a MessageListCall builder struct 34 | func (ms *MessageService) List(folderID string) *MessageListCall { 35 | return &MessageListCall{ 36 | service: ms, 37 | maxResults: 10, 38 | folderID: folderID, 39 | } 40 | } 41 | 42 | // MaxResults sets the $top query parameter for the message list call. 43 | func (mlc *MessageListCall) MaxResults(pageSize int64) *MessageListCall { 44 | mlc.maxResults = pageSize 45 | return mlc 46 | } 47 | 48 | // NextLink uses the link provided to set the $skip query parameter for the message list call. 49 | func (mlc *MessageListCall) NextLink(link string) *MessageListCall { 50 | mlc.nextLink = link 51 | return mlc 52 | } 53 | 54 | // StartTime sets the startDateTime query parameter for the message list call. 55 | func (mlc *MessageListCall) StartTime(start time.Time) *MessageListCall { 56 | mlc.startTime = start 57 | return mlc 58 | } 59 | 60 | // EndTime sets the endDateTime query parameter for the message list call. 61 | func (mlc *MessageListCall) EndTime(end time.Time) *MessageListCall { 62 | mlc.endTime = end 63 | return mlc 64 | } 65 | 66 | // Do executes the message list call, returning the message list result. 67 | func (mlc *MessageListCall) Do(ctx context.Context) (*MessageListResult, error) { 68 | params := map[string]interface{}{ 69 | "$top": mlc.maxResults, 70 | "$count": true, 71 | "startDateTime": mlc.startTime.Format(DefaultQueryDateTimeFormat), 72 | "endDateTime": mlc.endTime.Format(DefaultQueryDateTimeFormat), 73 | } 74 | if mlc.nextLink != "" { 75 | params["$skip"] = parsePageLink(mlc.nextLink, "$skip") 76 | } 77 | 78 | path := fmt.Sprintf("/mailFolders/%s%s", mlc.folderID, mlc.service.basePath) 79 | 80 | var result MessageListResult 81 | if _, err := mlc.service.session.Get(ctx, path, params, &result); err != nil { 82 | return nil, err 83 | } 84 | 85 | return &result, nil 86 | } 87 | -------------------------------------------------------------------------------- /models.go: -------------------------------------------------------------------------------- 1 | package outlook 2 | 3 | // User microsoft user object 4 | type User struct { 5 | FirstName string `json:"givenName,omitempty"` 6 | LastName string `json:"surName,omitempty"` 7 | Name string `json:"displayName,omitempty"` 8 | ID string `json:"id,omitempty"` 9 | Email string `json:"userPrincipalName,omitempty"` 10 | JobTitle string `json:"jobTitle,omitempty"` 11 | } 12 | 13 | // RefreshTokenRequest microsoft token request object 14 | type RefreshTokenRequest struct { 15 | ClientID string `json:"client_id"` 16 | ClientSecret string `json:"client_secret"` 17 | RefreshToken string `json:"refresh_token"` 18 | RedirectURI string `json:"redirect_uri"` 19 | Scope string `json:"scope"` 20 | GrantType string `json:"grant_type"` 21 | } 22 | 23 | // RefreshTokenResponse microsoft token response object 24 | type RefreshTokenResponse struct { 25 | AccessToken string `json:"access_token"` 26 | RefreshToken string `json:"refresh_token"` 27 | TokenType string `json:"token_type"` 28 | ExpiresIn int64 `json:"expires_in"` 29 | Scope string `json:"scope"` 30 | } 31 | 32 | // FolderListResult struct representing a response from the outlook mailFolders endpoint 33 | type FolderListResult struct { 34 | Context string `json:"@odata.context,omitempty"` 35 | NextLink string `json:"@odata.nextLink,omitempty"` 36 | Total int64 `json:"@odata.count,omitempty"` 37 | Value []*Folder `json:"value,omitempty"` 38 | } 39 | 40 | // Folder struct representing an outlook calendar object 41 | type Folder struct { 42 | ID string `json:"id,omitempty"` 43 | DisplayName string `json:"displayName,omitempty"` 44 | ParentFolderID string `json:"parentFolderId,omitempty"` 45 | ChildFolderCount int `json:"childFolderCount,omitempty"` 46 | UnreadItemCount int `json:"unreadItemCount,omitempty"` 47 | TotalItemCount int `json:"totalItemCount,omitempty"` 48 | } 49 | 50 | // MessageListResult struct representing a response from the outlook messages endpoint 51 | type MessageListResult struct { 52 | Context string `json:"@odata.context,omitempty"` 53 | NextLink string `json:"@odata.nextLink,omitempty"` 54 | Total int64 `json:"@odata.count,omitempty"` 55 | Value []*Message `json:"value,omitempty"` 56 | } 57 | 58 | // Message microsoft message object 59 | // TODO: Add all fields from outlook 60 | type Message struct { 61 | ID string `json:"id,omitempty"` 62 | MessageID string `json:"internetMessageId,omitempty"` 63 | CreatedOn string `json:"createdDateTime,omitempty"` 64 | ReceivedOn string `json:"receivedDateTime,omitempty"` 65 | SentOn string `json:"sentDateTime,omitempty"` 66 | Subject string `json:"subject,omitempty"` 67 | BodyPreview string `json:"bodyPreview,omitempty"` 68 | Importance string `json:"importance,omitempty"` 69 | ConversationID string `json:"conversationId,omitempty"` 70 | IsRead bool `json:"isread,omitempty"` 71 | Body *MessageBody `json:"body,omitempty"` 72 | Sender *Recipient `json:"sender,omitempty"` 73 | From *Recipient `json:"from,omitempty"` 74 | To []*Recipient `json:"toRecipients,omitempty"` 75 | CC []*Recipient `json:"ccRecipients,omitempty"` 76 | BCC []*Recipient `json:"bccRecipients,omitempty"` 77 | ReplyTo []*Recipient `json:"replyTo,omitempty"` 78 | } 79 | 80 | // BodyContentType enum 81 | const ( 82 | BodyContentTypeText = "TEXT" 83 | BodyContentTypeHTML = "HTML" 84 | ) 85 | 86 | // MessageBody microsoft body content object 87 | type MessageBody struct { 88 | ContentType string `json:"contentType,omitempty"` 89 | Content string `json:"content,omitempty"` 90 | } 91 | 92 | // Recipient microsoft message recipient object 93 | type Recipient struct { 94 | EmailAddress *EmailAddress `json:"emailAddress,omitempty"` 95 | } 96 | 97 | // EmailAddress microsoft message email object 98 | type EmailAddress struct { 99 | Name string `json:"name,omitempty"` 100 | Address string `json:"address,omitempty"` 101 | } 102 | 103 | // CalendarListResult you can tell by the way it is 104 | type CalendarListResult struct { 105 | Context string `json:"@odata.context,omitempty"` 106 | NextLink string `json:"@odata.nextLink,omitempty"` 107 | Total int64 `json:"@odata.count,omitempty"` 108 | Value []*Calendar `json:"value,omitempty"` 109 | } 110 | 111 | // Calendar outlook calendar object 112 | type Calendar struct { 113 | ID string `json:"id,omitempty"` 114 | Name string `json:"name,omitempty"` 115 | Color string `json:"color,omitempty"` 116 | CanShare bool `json:"canShare,omitempty"` 117 | CanViewPrivateItems bool `json:"canViewPrivateItems,omitempty"` 118 | CanEdit bool `json:"canEdit,omitempty"` 119 | Owner *EmailAddress `json:"owner,omitempty"` 120 | } 121 | 122 | // EventListResult you can tell by the way it is 123 | type EventListResult struct { 124 | Context string `json:"@odata.context,omitempty"` 125 | NextLink string `json:"@odata.nextLink,omitempty"` 126 | Total int64 `json:"@odata.count,omitempty"` 127 | Value []*Event `json:"value,omitempty"` 128 | } 129 | 130 | // Essentially, enums of possible values for outlook calendar events. Would like to change to iota+custom json serializer/deserializer. 131 | const ( 132 | // EventShowAs 133 | EventShowAsFree = "free" 134 | EventShowAsTentative = "tentative" 135 | EventShowAsBusy = "busy" 136 | EventShowAsOOF = "oof" 137 | EventShowAsElsewhere = "workingElsewhere" 138 | EventShowAsUnknown = "unknown" 139 | 140 | // EventType 141 | EventTypeSingleInstance = "singleInstance" 142 | EventTypeOccurrence = "occurrence" 143 | EventTypeException = "exception" 144 | EventTypeSeriesMaster = "seriesMaster" 145 | 146 | // EventSensitivity 147 | EventSensitivityNormal = "normal" 148 | EventSensitivityPersonal = "personal" 149 | EventSensitivityPrivate = "private" 150 | EventSensitivityConfidential = "confidential" 151 | 152 | // EventImportance 153 | EventImportanceLow = "low" 154 | EventImportanceNormal = "normal" 155 | EventImportanceHigh = "high" 156 | ) 157 | 158 | // Event microsoft event object 159 | // TODO: Add all fields from outlook 160 | type Event struct { 161 | ID string `json:"id,omitempty"` 162 | CreatedOn string `json:"createdDateTime,omitempty"` 163 | UpdatedOn string `json:"lastModifiedDateTime,omitempty"` 164 | ICalUID string `json:"iCalUId,omitempty"` 165 | Categories []string `json:"categories,omitempty"` 166 | Subject string `json:"subject,omitempty"` 167 | BodyPreview string `json:"bodyPreview,omitempty"` 168 | Importance string `json:"importance,omitempty"` 169 | IsOrganizer bool `json:"isOrganizer,omitempty"` 170 | IsCancelled bool `json:"isCancelled,omitempty"` 171 | SeriesID string `json:"seriesMasterId,omitempty"` 172 | Type string `json:"type,omitempty"` 173 | Body *MessageBody `json:"body,omitempty"` 174 | Start *DateTimeTimeZone `json:"start,omitempty"` 175 | OriginalStart string `json:"originalStart,omitempty"` // YYYY-mm-ddT00:00:00Z 176 | OriginalStartTimezone string `json:"originalStartTimeZone,omitempty"` 177 | End *DateTimeTimeZone `json:"end,omitempty"` 178 | AllDay bool `json:"isAllDay,omitempty"` 179 | Location *Location `json:"location,omitempty"` 180 | Locations []*Location `json:"locations,omitempty"` 181 | Attendees []*Attendee `json:"attendees,omitempty"` 182 | Organizer *Recipient `json:"organizer,omitempty"` 183 | ResponseStatus *ResponseStatus `json:"responseStatus,omitempty"` 184 | WebLink string `json:"webLink,omitempty"` 185 | OnlineMeetingURL string `json:"onlineMeetingUrl,omitempty"` 186 | ShowAs string `json:"showAs,omitempty"` 187 | Sensitivity string `json:"sensitivity,omitempty"` 188 | ResponseRequested bool `json:"responseRequested,omitempty"` 189 | ReminderMinutesBeforeStart int `json:"reminderMinutesBeforeStart,omitempty"` 190 | Recurrence *PatternedRecurrence `json:"recurrence,omitempty"` 191 | ReminderOn bool `json:"isReminderOn,omitempty"` 192 | HasAttachments bool `json:"hasAttachments,omitempty"` 193 | } 194 | 195 | // ResponseStatus something 196 | type ResponseStatus struct { 197 | Response string `json:"response,omitempty"` 198 | Time string `json:"time,omitempty"` 199 | } 200 | 201 | // DateTimeTimeZone microsoft event datetime-timezone object 202 | type DateTimeTimeZone struct { 203 | DateTime string `json:"dateTime,omitempty"` 204 | Timezone string `json:"timeZone,omitempty"` 205 | } 206 | 207 | // Location microsoft event location object 208 | type Location struct { 209 | DisplayName string `json:"displayName,omitempty"` 210 | Address *Address `json:"address,omitempty"` 211 | Type string `json:"locationType,omitempty"` 212 | } 213 | 214 | // Address microsoft event location address object 215 | type Address struct { 216 | Street string `json:"street,omitempty"` 217 | City string `json:"city,omitempty"` 218 | State string `json:"state,omitempty"` 219 | Country string `json:"countryOrRegion,omitempty"` 220 | Postal string `json:"postalCode,omitempty"` 221 | } 222 | 223 | // Attendee microsoft event attendee object 224 | type Attendee struct { 225 | Type string `json:"type,omitempty"` 226 | Status *ResponseStatus `json:"status,omitempty"` 227 | EmailAddress *EmailAddress `json:"emailAddress,omitempty"` 228 | } 229 | 230 | // PatternedRecurrence microsoft recurrence definition. 231 | type PatternedRecurrence struct { 232 | Pattern *RecurrencePattern `json:"pattern,omitempty"` 233 | Range *RecurrenceRange `json:"range,omitempty"` 234 | } 235 | 236 | // RecurrencePattern enums for recurrence definition 237 | const ( 238 | // RecurrencePatternType 239 | RecurrencePatternTypeDaily = "daily" 240 | RecurrencePatternTypeWeekly = "weekly" 241 | RecurrencePatternTypeAbsoluteMonthly = "absoluteMonthly" 242 | RecurrencePatternTypeRelativeMonthly = "relativeMonthly" 243 | RecurrencePatternTypeAbsoluteYearly = "absoluteYearly" 244 | RecurrencePatternTypeRelativeYearly = "relativeYearly" 245 | 246 | // RecurrencePatternIndex 247 | RecurrencePatternIndexFirst = "first" 248 | RecurrencePatternIndexSecond = "second" 249 | RecurrencePatternIndexThird = "third" 250 | RecurrencePatternIndexFourth = "fourth" 251 | RecurrencePatternIndexLast = "last" 252 | ) 253 | 254 | // RecurrencePattern microsoft event recurrence pattern definition. 255 | type RecurrencePattern struct { 256 | DayOfMonth int `json:"dayOfMonth,omitempty"` 257 | DaysOfWeek []string `json:"daysOfWeek,omitempty"` 258 | FirstDayOfWeek string `json:"firstDayOfWeek,omitempty"` 259 | Index string `json:"index,omitempty"` 260 | Interval int `json:"interval,omitempty"` 261 | Month int `json:"month,omitempty"` 262 | Type string `json:"type,omitempty"` 263 | } 264 | 265 | // RecurrenceRangeType enum 266 | const ( 267 | RecurrenceRangeTypeEndDate = "endDate" 268 | RecurrenceRangeTypeNoEnd = "noEnd" 269 | RecurrenceRangeTypeNumbered = "numbered" 270 | ) 271 | 272 | // RecurrenceRange microsoft event recurrence range definition. 273 | type RecurrenceRange struct { 274 | EndDate string `json:"endDate,omitempty"` // FORMAT - YYYY-mm-dd 275 | NumberOfOccurrences int `json:"numberOfOccurrences,omitempty"` 276 | RecurrenceTimezone string `json:"recurrenceTimeZone,omitempty"` 277 | StartDate string `json:"startDate,omitempty"` 278 | Type string `json:"type,omitempty"` 279 | } 280 | -------------------------------------------------------------------------------- /outlook.go: -------------------------------------------------------------------------------- 1 | package outlook 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "errors" 8 | "fmt" 9 | "io" 10 | "net/http" 11 | "net/url" 12 | "strings" 13 | "time" 14 | ) 15 | 16 | const ( 17 | // ClientVersion the current version of this sdk 18 | ClientVersion = "0.1.0" 19 | // DefaultBaseURL the root host url for the microsoft outlook api 20 | DefaultBaseURL = "https://graph.microsoft.com/v1.0" 21 | // DefaultOAuthTokenURL the url used to exchange a user's refreshToken for a usable accessToken 22 | DefaultOAuthTokenURL = "https://login.microsoftonline.com/common/oauth2/v2.0/token" 23 | // DefaultAuthScopes the set of permissions the client will request from the user 24 | DefaultAuthScopes = "mail.read calendars.read user.read offline_access" 25 | // DefaultQueryDateTimeFormat time format for the datetime query parameters used in outlook 26 | DefaultQueryDateTimeFormat = "2006-01-02T15:04:05Z" 27 | 28 | mediaType = "application/json" 29 | ) 30 | 31 | var ( 32 | // ErrNoDeltaLink error when our email paging fails to return a delta token at the end. 33 | ErrNoDeltaLink = errors.New("no delta link on response") 34 | 35 | // DefaultClient the http client that the sdk will use to make calls. 36 | DefaultClient = &http.Client{Timeout: time.Second * 60} 37 | 38 | // DefaultUserAgent the user agent to get passed in request headers on each call 39 | DefaultUserAgent = fmt.Sprintf("go-outlook/%s", ClientVersion) 40 | ) 41 | 42 | // Client manages communication with microsoft's graph api, specifically for Mail and Calendar. 43 | type Client struct { 44 | client *http.Client 45 | baseURL *url.URL 46 | userAgent string 47 | mediaType string 48 | appID string 49 | appSecret string 50 | redirectURI string 51 | scope string 52 | } 53 | 54 | // ClientOpt functions to configure options on a Client. 55 | type ClientOpt func(*Client) 56 | 57 | // SetClientAppID returns a ClientOpt function which set the clients AppID. 58 | func SetClientAppID(appID string) ClientOpt { 59 | return func(c *Client) { 60 | c.appID = appID 61 | } 62 | } 63 | 64 | // SetClientAppSecret returns a ClientOpt function which set the clients App Secret. 65 | func SetClientAppSecret(secret string) ClientOpt { 66 | return func(c *Client) { 67 | c.appSecret = secret 68 | } 69 | } 70 | 71 | // SetClientRedirectURI returns a ClientOpt function which set the clients redirectURI. 72 | func SetClientRedirectURI(uri string) ClientOpt { 73 | return func(c *Client) { 74 | c.redirectURI = uri 75 | } 76 | } 77 | 78 | // SetClientScope returns a ClientOpt function which set the clients auth scope. 79 | func SetClientScope(scope string) ClientOpt { 80 | return func(c *Client) { 81 | c.scope = scope 82 | } 83 | } 84 | 85 | // SetClientMediaType returns a ClientOpt function which sets the clients mediaType. 86 | func SetClientMediaType(mType string) ClientOpt { 87 | return func(c *Client) { 88 | c.mediaType = mType 89 | } 90 | } 91 | 92 | // NewClient returns a new instance of a Client with the given options set. 93 | func NewClient(opts ...ClientOpt) (*Client, error) { 94 | baseURL, err := url.Parse(DefaultBaseURL) 95 | if err != nil { 96 | return nil, err 97 | } 98 | client := &Client{ 99 | client: DefaultClient, 100 | baseURL: baseURL, 101 | userAgent: DefaultUserAgent, 102 | scope: DefaultAuthScopes, 103 | mediaType: mediaType, 104 | } 105 | for _, opt := range opts { 106 | opt(client) 107 | } 108 | return client, nil 109 | } 110 | 111 | // SetAppID fluent configuration of the client's microsoft AppID. 112 | func (client *Client) SetAppID(appID string) *Client { 113 | client.appID = appID 114 | return client 115 | } 116 | 117 | // SetAppSecret fluent configuration of the client's microsoft App Secret. 118 | func (client *Client) SetAppSecret(secret string) *Client { 119 | client.appSecret = secret 120 | return client 121 | } 122 | 123 | // SetRedirectURI fluent configuration of the client's microsoft RedirectURI. 124 | func (client *Client) SetRedirectURI(uri string) *Client { 125 | client.redirectURI = uri 126 | return client 127 | } 128 | 129 | // SetScope fluent configuration of the client's microsoft auth Scope. 130 | func (client *Client) SetScope(scope string) *Client { 131 | client.scope = scope 132 | return client 133 | } 134 | 135 | // SetMediaType fluent configuration of the client's mediaType. 136 | func (client *Client) SetMediaType(mType string) *Client { 137 | client.mediaType = mType 138 | return client 139 | } 140 | 141 | // NewRequest creates a new request with some reasonable defaults based on the client. 142 | func (client *Client) NewRequest(ctx context.Context, method, path string, body interface{}) (*http.Request, error) { 143 | var fullURL string 144 | pathURL, err := url.Parse(path) 145 | if err != nil { 146 | return nil, err 147 | } 148 | if pathURL.Hostname() != "" { 149 | fullURL = path 150 | } else { 151 | fullURL = fmt.Sprintf("%s%s", client.baseURL.String(), path) 152 | } 153 | 154 | encodedBody := new(bytes.Buffer) 155 | if body != nil { 156 | switch client.mediaType { 157 | case "application/json": 158 | if err := json.NewEncoder(encodedBody).Encode(body); err != nil { 159 | return nil, err 160 | } 161 | case "application/x-www-form-urlencoded": 162 | if v, ok := body.(url.Values); ok { 163 | bodyReader := strings.NewReader(v.Encode()) 164 | if _, err := io.Copy(encodedBody, bodyReader); err != nil { 165 | return nil, err 166 | } 167 | } else { 168 | return nil, fmt.Errorf("Body must be of type url.Values when Content-Type is set to application/x-www-form-urlencoded") 169 | } 170 | } 171 | } 172 | 173 | req, err := http.NewRequest(method, fullURL, encodedBody) 174 | if err != nil { 175 | return nil, err 176 | } 177 | 178 | req.Header.Add("Content-Type", client.mediaType) 179 | req.Header.Add("Accept", mediaType) 180 | req.Header.Add("User-Agent", client.userAgent) 181 | 182 | return req, nil 183 | } 184 | 185 | // Do executes the given http request and will bind the response body with v. Returns the http response as well as any error. 186 | func (client *Client) Do(ctx context.Context, req *http.Request, v interface{}) (*http.Response, error) { 187 | req = req.WithContext(ctx) 188 | response, err := client.client.Do(req) 189 | if err != nil { 190 | return nil, err 191 | } 192 | 193 | defer func() { 194 | if closeErr := response.Body.Close(); closeErr != nil { 195 | err = closeErr 196 | } 197 | }() 198 | 199 | err = checkResponse(response) 200 | if err != nil { 201 | return response, err 202 | } 203 | 204 | if v != nil { 205 | if w, ok := v.(io.Writer); ok { 206 | _, err = io.Copy(w, response.Body) 207 | if err != nil { 208 | return response, err 209 | } 210 | } else { 211 | err = json.NewDecoder(response.Body).Decode(v) 212 | if err != nil { 213 | return response, err 214 | } 215 | } 216 | } 217 | 218 | return response, err 219 | } 220 | 221 | // NewSession returns a new instance of a Session using this client and the given refreshToken. 222 | func (client *Client) NewSession(refreshToken string) (*Session, error) { 223 | session, err := NewSession(client, refreshToken) 224 | if err != nil { 225 | return nil, err 226 | } 227 | return session, nil 228 | } 229 | -------------------------------------------------------------------------------- /session.go: -------------------------------------------------------------------------------- 1 | package outlook 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "net/url" 8 | ) 9 | 10 | // Session manages communication to microsoft's graph api as an authenticated user. 11 | type Session struct { 12 | client *Client 13 | basePath string 14 | accessToken string 15 | refreshToken string 16 | } 17 | 18 | // NewSession returns a new instance of a Session. 19 | func NewSession(client *Client, refreshToken string) (*Session, error) { 20 | session := &Session{ 21 | client: client, 22 | basePath: "/me", 23 | refreshToken: refreshToken, 24 | } 25 | 26 | if err := session.refreshAccessToken(); err != nil { 27 | return nil, err 28 | } 29 | 30 | return session, nil 31 | } 32 | 33 | func (session *Session) query(ctx context.Context, method, url string, params map[string]interface{}, data interface{}, result interface{}) (*http.Response, error) { 34 | var queryString string 35 | if params != nil { 36 | queryString = createQueryString(params) 37 | } 38 | 39 | path := fmt.Sprintf("%s%s%s", session.basePath, url, queryString) 40 | 41 | req, err := session.client.NewRequest(ctx, method, path, data) 42 | if err != nil { 43 | return nil, err 44 | } 45 | 46 | if session.accessToken == "" { 47 | return nil, ErrNoAccessToken 48 | } 49 | 50 | req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", session.accessToken)) 51 | 52 | // May want to detect failures due to invalid or expired tokens, then retry after attempting to refresh the token 53 | return session.client.Do(ctx, req, result) 54 | } 55 | 56 | // Get performs a get request to microsofts api with the underlying client and the sessions accessToken for authorization. 57 | func (session *Session) Get(ctx context.Context, url string, params map[string]interface{}, result interface{}) (*http.Response, error) { 58 | return session.query(ctx, http.MethodGet, url, params, nil, result) 59 | } 60 | 61 | // Post performs a post request to microsofts api with the underlying client and the sessions accessToken for authorization. 62 | func (session *Session) Post(ctx context.Context, url string, data interface{}, result interface{}) (*http.Response, error) { 63 | return session.query(ctx, http.MethodPost, url, nil, data, result) 64 | } 65 | 66 | // Patch performs a patch request to microsofts api with the underlying client and the sessions accessToken for authorization. 67 | func (session *Session) Patch(ctx context.Context, url string, data interface{}, result interface{}) (*http.Response, error) { 68 | return session.query(ctx, http.MethodPatch, url, nil, data, result) 69 | } 70 | 71 | // Delete performs a delete request to microsofts api with the underlying client and the sessions accessToken for authorization. 72 | func (session *Session) Delete(ctx context.Context, url string, params map[string]interface{}, result interface{}) (*http.Response, error) { 73 | return session.query(ctx, http.MethodDelete, url, params, nil, result) 74 | } 75 | 76 | // Calendars returns an instance of a CalendarService using this session. 77 | func (session *Session) Calendars() *CalendarService { 78 | return NewCalendarService(session) 79 | } 80 | 81 | // Events returns an instance of a EventService using this session. 82 | func (session *Session) Events() *EventService { 83 | return NewEventService(session) 84 | } 85 | 86 | // Folders returns an instance of a FolderService using this session. 87 | func (session *Session) Folders() *FolderService { 88 | return NewFolderService(session) 89 | } 90 | 91 | // Messages returns an instance of a MessageService using this session. 92 | func (session *Session) Messages() *MessageService { 93 | return NewMessageService(session) 94 | } 95 | 96 | func (session *Session) refreshAccessToken() error { 97 | body := url.Values{} 98 | body.Set("client_id", session.client.appID) 99 | body.Set("client_secret", session.client.appSecret) 100 | body.Set("refresh_token", session.refreshToken) 101 | body.Set("redirect_uri", session.client.redirectURI) 102 | body.Set("scope", session.client.scope) 103 | body.Set("grant_type", "refresh_token") 104 | 105 | // I suppose it's possible for this to change the media type of other requests being made at essentially the same time, will update sometime 106 | session.client.SetMediaType("application/x-www-form-urlencoded") 107 | 108 | req, err := session.client.NewRequest(context.Background(), http.MethodPost, DefaultOAuthTokenURL, body) 109 | if err != nil { 110 | return err 111 | } 112 | 113 | session.client.SetMediaType(mediaType) 114 | 115 | var tokenRes RefreshTokenResponse 116 | if _, err := session.client.Do(context.Background(), req, &tokenRes); err != nil { 117 | return err 118 | } 119 | 120 | session.accessToken = tokenRes.AccessToken 121 | 122 | return nil 123 | } 124 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | package outlook 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "net/http" 7 | "net/url" 8 | "strconv" 9 | "time" 10 | ) 11 | 12 | func checkResponse(res *http.Response) error { 13 | status := res.StatusCode 14 | if status >= 200 && status < 300 { 15 | return nil 16 | } 17 | 18 | statusErr := &ErrStatusCode{Code: status} 19 | data, err := ioutil.ReadAll(res.Body) 20 | if err != nil { 21 | return err 22 | } 23 | if len(data) > 0 { 24 | statusErr.Message = string(data) 25 | } 26 | if statusErr.Code == 429 { 27 | rawRetrySecs := res.Header.Get("Retry-After") 28 | if rawRetrySecs != "" { 29 | retrySecs, _ := strconv.ParseInt(rawRetrySecs, 10, 64) 30 | statusErr.SuggestedRetryDuration = time.Duration(retrySecs) * time.Second 31 | } 32 | } 33 | 34 | return statusErr 35 | } 36 | 37 | func createQueryString(params map[string]interface{}) string { 38 | query := url.Values{} 39 | for key, val := range params { 40 | query.Set(key, fmt.Sprintf("%v", val)) 41 | } 42 | finalQuery := query.Encode() 43 | if finalQuery == "" { 44 | return "" 45 | } 46 | return fmt.Sprintf("?%s", finalQuery) 47 | } 48 | 49 | func parsePageLink(link, key string) string { 50 | parsed, _ := url.Parse(link) 51 | q := parsed.Query() 52 | return q.Get(key) 53 | } 54 | --------------------------------------------------------------------------------